diff --git a/changelog.md b/changelog.md index 8f6975f..5623be6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-05-30 - 7.4.3 - fix(dnsserver) +Fix DNSSEC RRset signing, SOA record timeout issues, and add configurable primary nameserver support. + +- Fixed DNSSEC to sign entire RRsets together instead of individual records (one RRSIG per record type) +- Fixed SOA record serialization by implementing proper wire format encoding in serializeRData method +- Fixed RRSIG generation by using correct field names (signersName) and types (string typeCovered) +- Added configurable primary nameserver via primaryNameserver option in IDnsServerOptions +- Enhanced test coverage with comprehensive SOA and DNSSEC test scenarios + ## 2025-05-30 - 7.4.2 - fix(dnsserver) Enable multiple DNS record support by removing the premature break in processDnsRequest. Now the DNS server aggregates answers from all matching handlers for NS, A, and TXT records, and improves NS record serialization for DNSSEC. diff --git a/package.json b/package.json index 6e8a2a4..249067d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@push.rocks/smartdns", - "version": "7.4.3", + "version": "7.4.4", "private": false, "description": "A robust TypeScript library providing advanced DNS management and resolution capabilities including support for DNSSEC, custom DNS servers, and integration with various DNS providers.", "exports": { diff --git a/test/test.multiplerecords.fixed.ts b/test/test.multiplerecords.fixed.ts index de363cb..f3b7b84 100644 --- a/test/test.multiplerecords.fixed.ts +++ b/test/test.multiplerecords.fixed.ts @@ -396,9 +396,9 @@ tap.test('should handle DNSSEC correctly with multiple records', async () => { console.log('NS records:', nsAnswers.length); console.log('RRSIG records:', rrsigAnswers.length); - // With DNSSEC, each NS record should have an associated RRSIG + // With DNSSEC RRset signing, all NS records share ONE RRSIG (entire RRset signed together) expect(nsAnswers.length).toEqual(2); - expect(rrsigAnswers.length).toEqual(2); + expect(rrsigAnswers.length).toEqual(1); await stopServer(dnsServer); dnsServer = null; diff --git a/test/test.soa.debug.ts b/test/test.soa.debug.ts new file mode 100644 index 0000000..2d3caca --- /dev/null +++ b/test/test.soa.debug.ts @@ -0,0 +1,269 @@ +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(); \ No newline at end of file diff --git a/test/test.soa.final.ts b/test/test.soa.final.ts new file mode 100644 index 0000000..73c36a5 --- /dev/null +++ b/test/test.soa.final.ts @@ -0,0 +1,271 @@ +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 = 8900; +let nextUdpPort = 8901; + +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('SOA records work for all scenarios', 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', + primaryNameserver: 'ns.example.com', + }); + + // Register SOA handler for the zone + dnsServer.registerHandler('example.com', ['SOA'], (question) => { + console.log('SOA handler called for:', question.name); + return { + name: question.name, + type: 'SOA', + class: 'IN', + ttl: 3600, + data: { + mname: 'ns.example.com', + rname: 'admin.example.com', + serial: 2024010101, + refresh: 3600, + retry: 600, + expire: 604800, + minimum: 86400, + }, + }; + }); + + // Register some other records + dnsServer.registerHandler('example.com', ['A'], (question) => { + return { + name: question.name, + type: 'A', + class: 'IN', + ttl: 300, + data: '192.168.1.1', + }; + }); + + await dnsServer.start(); + + const client = dgram.createSocket('udp4'); + + // Test 1: Direct SOA query + console.log('\n--- Test 1: Direct SOA query ---'); + const soaQuery = dnsPacket.encode({ + type: 'query', + id: 1, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'example.com', + type: 'SOA', + class: 'IN', + }, + ], + }); + + let response = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + client.removeAllListeners(); + }); + + client.on('error', (err) => { + clearTimeout(timeout); + reject(err); + client.close(); + }); + + client.send(soaQuery, udpPort, 'localhost'); + }); + + console.log('Direct SOA query response:', response.answers.length, 'answers'); + expect(response.answers.length).toEqual(1); + expect(response.answers[0].type).toEqual('SOA'); + + // Test 2: Non-existent domain query (should get SOA in authority) + console.log('\n--- Test 2: Non-existent domain query ---'); + const nxQuery = dnsPacket.encode({ + type: 'query', + id: 2, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'nonexistent.example.com', + type: 'A', + class: 'IN', + }, + ], + }); + + response = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + client.removeAllListeners(); + }); + + client.send(nxQuery, udpPort, 'localhost'); + }); + + console.log('Non-existent query response:', response.answers.length, 'answers'); + const soaAnswers = response.answers.filter(a => a.type === 'SOA'); + expect(soaAnswers.length).toEqual(1); + + // Test 3: SOA with DNSSEC + console.log('\n--- Test 3: SOA query with DNSSEC ---'); + const dnssecQuery = dnsPacket.encode({ + type: 'query', + id: 3, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'example.com', + type: 'SOA', + class: 'IN', + }, + ], + additionals: [ + { + name: '.', + type: 'OPT', + ttl: 0, + flags: 0x8000, // DO bit + data: Buffer.alloc(0), + } as any, + ], + }); + + response = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + client.removeAllListeners(); + }); + + client.send(dnssecQuery, udpPort, 'localhost'); + }); + + console.log('DNSSEC SOA query response:', response.answers.length, 'answers'); + console.log('Answer types:', response.answers.map(a => a.type)); + expect(response.answers.length).toEqual(2); // SOA + RRSIG + expect(response.answers.some(a => a.type === 'SOA')).toEqual(true); + expect(response.answers.some(a => a.type === 'RRSIG')).toEqual(true); + + client.close(); + await stopServer(dnsServer); + dnsServer = null; +}); + +tap.test('Configurable primary nameserver works correctly', async () => { + const httpsData = await tapNodeTools.createHttpsCert(); + const udpPort = getUniqueUdpPort(); + + dnsServer = new smartdns.DnsServer({ + httpsKey: httpsData.key, + httpsCert: httpsData.cert, + httpsPort: getUniqueHttpsPort(), + udpPort: udpPort, + dnssecZone: 'test.com', + primaryNameserver: 'master.test.com', + }); + + await dnsServer.start(); + + const client = dgram.createSocket('udp4'); + + const query = dnsPacket.encode({ + type: 'query', + id: 1, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'nonexistent.test.com', + type: 'A', + class: 'IN', + }, + ], + }); + + const response = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + }); + + client.on('error', (err) => { + clearTimeout(timeout); + reject(err); + client.close(); + }); + + client.send(query, udpPort, 'localhost'); + }); + + const soaAnswers = response.answers.filter(a => a.type === 'SOA'); + console.log('✅ Configured primary nameserver:', (soaAnswers[0] as any).data.mname); + expect((soaAnswers[0] as any).data.mname).toEqual('master.test.com'); + + client.close(); + await stopServer(dnsServer); + dnsServer = null; +}); + +export default tap.start(); \ No newline at end of file diff --git a/test/test.soa.simple.ts b/test/test.soa.simple.ts new file mode 100644 index 0000000..173f46b --- /dev/null +++ b/test/test.soa.simple.ts @@ -0,0 +1,201 @@ +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 = 8800; +let nextUdpPort = 8801; + +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('Simple SOA query without DNSSEC', 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'); + + // Query for a non-existent domain WITHOUT DNSSEC + const query = dnsPacket.encode({ + type: 'query', + id: 1, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'nonexistent.example.com', + type: 'A', + class: 'IN', + }, + ], + }); + + const responsePromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + try { + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + } catch (e) { + reject(e); + } + 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(); + } + }); + }); + + const dnsResponse = await responsePromise; + console.log('✅ SOA response without DNSSEC received'); + + 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.mname); + + await stopServer(dnsServer); + dnsServer = null; +}); + +tap.test('Direct SOA query without DNSSEC', 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 direct SOA handler + dnsServer.registerHandler('example.com', ['SOA'], (question) => { + 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: 2, + flags: dnsPacket.RECURSION_DESIRED, + questions: [ + { + name: 'example.com', + type: 'SOA', + class: 'IN', + }, + ], + }); + + const responsePromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + client.close(); + reject(new Error('Query timed out')); + }, 2000); + + client.on('message', (msg) => { + clearTimeout(timeout); + try { + const dnsResponse = dnsPacket.decode(msg); + resolve(dnsResponse); + } catch (e) { + reject(e); + } + 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(); + } + }); + }); + + const dnsResponse = await responsePromise; + console.log('✅ Direct SOA query succeeded'); + + const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); + expect(soaAnswers.length).toEqual(1); + + await stopServer(dnsServer); + dnsServer = null; +}); + +export default tap.start(); \ No newline at end of file diff --git a/test/test.soa.timeout.ts b/test/test.soa.timeout.ts new file mode 100644 index 0000000..2bbf239 --- /dev/null +++ b/test/test.soa.timeout.ts @@ -0,0 +1,224 @@ +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 { execSync } from 'child_process'; + +import * as smartdns from '../ts_server/index.js'; + +let dnsServer: smartdns.DnsServer; + +// Port management for tests +const testPort = 8753; + +// 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 timeout with real dig command', async (tools) => { + const httpsData = await tapNodeTools.createHttpsCert(); + + dnsServer = new smartdns.DnsServer({ + httpsKey: httpsData.key, + httpsCert: httpsData.cert, + httpsPort: 8752, + udpPort: testPort, + dnssecZone: 'example.com', + }); + + await dnsServer.start(); + console.log(`DNS server started on port ${testPort}`); + + // Test with dig command + try { + console.log('Testing SOA query with dig...'); + const result = execSync(`dig @localhost -p ${testPort} example.com SOA +timeout=3`, { encoding: 'utf8' }); + console.log('Dig SOA query result:', result); + + // Check if we got an answer section + expect(result).toInclude('ANSWER SECTION'); + expect(result).toInclude('SOA'); + } catch (error) { + console.error('Dig command failed:', error.message); + throw error; + } + + // Test nonexistent domain SOA + try { + console.log('Testing nonexistent domain SOA query with dig...'); + const result = execSync(`dig @localhost -p ${testPort} nonexistent.example.com A +timeout=3`, { encoding: 'utf8' }); + console.log('Dig nonexistent query result:', result); + + // Should get AUTHORITY section with SOA + expect(result).toInclude('AUTHORITY SECTION'); + } catch (error) { + console.error('Dig nonexistent query failed:', error.message); + throw error; + } + + await stopServer(dnsServer); + dnsServer = null; +}); + +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); + + // With the fix, SOA should have its RRSIG + if (soaAnswers.length > 0) { + expect(rrsigAnswers.length).toBeGreaterThan(0); + } + } catch (error) { + console.error('DNSSEC SOA query failed:', error); + throw error; + } + + await stopServer(dnsServer); + dnsServer = null; +}); + +tap.test('Check DNSSEC signing performance for SOA', async () => { + const httpsData = await tapNodeTools.createHttpsCert(); + + dnsServer = new smartdns.DnsServer({ + httpsKey: httpsData.key, + httpsCert: httpsData.cert, + httpsPort: 8756, + udpPort: 8757, + dnssecZone: 'example.com', + }); + + // Time SOA serialization + const soaData = { + mname: 'ns1.example.com', + rname: 'hostmaster.example.com', + serial: 2024010101, + refresh: 3600, + retry: 600, + expire: 604800, + minimum: 86400, + }; + + console.log('Testing SOA serialization performance...'); + const serializeStart = Date.now(); + + try { + // @ts-ignore - accessing private method for testing + const serialized = dnsServer.serializeRData('SOA', soaData); + const serializeTime = Date.now() - serializeStart; + console.log(`SOA serialization took ${serializeTime}ms`); + + // Test DNSSEC signing + const signStart = Date.now(); + // @ts-ignore - accessing private property + const signature = dnsServer.dnsSec.signData(serialized); + const signTime = Date.now() - signStart; + console.log(`DNSSEC signing took ${signTime}ms`); + + expect(serializeTime).toBeLessThan(100); // Should be fast + expect(signTime).toBeLessThan(500); // Signing can take longer but shouldn't timeout + } catch (error) { + console.error('Performance test failed:', error); + throw error; + } +}); + +export default tap.start(); \ No newline at end of file diff --git a/ts_server/classes.dnsserver.ts b/ts_server/classes.dnsserver.ts index 3115b2f..12781d7 100644 --- a/ts_server/classes.dnsserver.ts +++ b/ts_server/classes.dnsserver.ts @@ -637,6 +637,12 @@ export class DnsServer { }, }; response.answers.push(soaAnswer as plugins.dnsPacket.Answer); + + // Add SOA record to DNSSEC signing map if DNSSEC is requested + if (dnssecRequested) { + const soaKey = `${question.name}:SOA`; + rrsetMap.set(soaKey, [soaAnswer]); + } } } @@ -684,6 +690,17 @@ export class DnsServer { // Sign the RRset const signature = this.dnsSec.signData(rrsetBuffer); + + // Ensure all fields are defined + if (!signerName || !signature) { + console.error('RRSIG generation error - missing fields:', { + signerName, + signature: signature ? 'present' : 'missing', + algorithm, + keyTag, + type + }); + } // Construct the RRSIG record const rrsig: DnsAnswer = { @@ -692,15 +709,15 @@ export class DnsServer { class: 'IN', ttl, data: { - typeCovered: type, // Changed to type string + typeCovered: type, // dns-packet expects the string type algorithm, - labels: name.split('.').length - 1, + labels: name.split('.').filter(l => l.length > 0).length, // Fix label count originalTTL: ttl, expiration, inception, keyTag, - signerName, - signature: signature, + signersName: signerName || this.options.dnssecZone, // Note: signersName with 's' + signature: signature || Buffer.alloc(0), // Fallback to empty buffer }, };