feat(dnsserver): Enhance DNSSEC RRset signing and add configurable primary nameserver

- Fix DNSSEC to properly sign entire RRsets together instead of individual records
- Implement proper SOA record serialization according to RFC 1035
- Add primaryNameserver option to IDnsServerOptions for customizable SOA mname field
- Add comprehensive tests for DNSSEC RRset signing and SOA record handling
- Update documentation with v7.4.3 improvements

Co-Authored-By: User <user@example.com>
This commit is contained in:
Philipp Kunz 2025-05-30 18:20:55 +00:00
parent 4e37bc9bc0
commit b87cbbee5c
6 changed files with 871 additions and 46 deletions

View File

@ -104,8 +104,12 @@ The test suite demonstrates:
- Let's Encrypt integration requires proper domain authorization
- Handler patterns should be carefully designed to avoid open resolvers
## Known Issues
## Recent Improvements (v7.4.3)
1. **DNSSEC RRSIG Generation**: When multiple records of the same type are returned, DNSSEC signing may encounter issues with the current implementation
2. **SOA Record Prefix**: The server hardcodes 'ns1.' prefix for SOA mname field which may not match actual nameserver names
3. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records
1. **DNSSEC RRset Signing**: Fixed to properly sign entire RRsets together instead of individual records
2. **SOA Record Serialization**: Implemented proper SOA record encoding for DNSSEC compatibility
3. **Configurable Primary Nameserver**: Added `primaryNameserver` option to customize SOA mname field
## Known Limitations
1. **Handler Deduplication**: If the same handler is registered multiple times, it will contribute duplicate records (this may be desired behavior for some use cases)

View File

@ -4,9 +4,17 @@ Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
## Critical Issue: Support Multiple DNS Records of Same Type
### Current Status: Planning
### Current Status: ✅ IMPLEMENTED (v7.4.2)
**Priority: HIGH** - This issue blocks proper DNS server operation and domain registration
## All Issues Fixed (v7.4.3)
### Successfully Implemented:
1. ✅ **Multiple DNS Records Support** (v7.4.2) - Core fix allowing multiple handlers to contribute records
2. ✅ **DNSSEC RRset Signing** - Now signs entire RRsets together instead of individual records
3. ✅ **SOA Record Serialization** - Proper SOA record encoding for DNSSEC compatibility
4. ✅ **Configurable Primary Nameserver** - Added `primaryNameserver` option to IDnsServerOptions
### Problem Summary
The DNS server currently exits after finding the first matching handler for a query, preventing it from serving multiple records of the same type (e.g., multiple NS records, multiple A records for round-robin, multiple TXT records).
@ -89,9 +97,69 @@ answered = true;
// Continue processing other handlers to allow multiple records
```
### Additional Improvements to Consider
1. Fix DNSSEC RRSIG generation for multiple records
2. Fix SOA record timeout issues
3. Make DNSSEC zone prefix configurable (remove hardcoded 'ns1.')
4. Improve error handling for edge cases
5. Consider handler interface enhancement to return arrays
## Next Steps and Future Improvements
### Released in v7.4.2
The critical issue of supporting multiple DNS records of the same type has been successfully implemented and released in version 7.4.2.
## Comprehensive Fix Plan for Remaining Issues
Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`
### Outstanding Issues to Address
#### 1. DNSSEC RRSIG Generation for Multiple Records
**Status**: Pending
**Priority**: Medium
**Issue**: When multiple records of the same type are returned with DNSSEC enabled, the RRSIG generation may encounter issues with the current implementation. Each record gets its own RRSIG instead of signing the entire RRset together.
**Implementation Plan**:
1. Modify `processDnsRequest` to collect all records of the same type before signing
2. Create a map to group answers by record type
3. After all handlers have been processed, sign each RRset as a whole
4. Generate one RRSIG per record type (not per record)
5. Update tests to verify proper DNSSEC RRset signing
6. Ensure canonical ordering of records in RRset for consistent signatures
**Code Changes**:
- Refactor the DNSSEC signing logic in `processDnsRequest`
- Move RRSIG generation outside the handler loop
- Group records by type before signing
#### 2. SOA Record Timeout Issues
**Status**: Not Started
**Priority**: Low
**Issue**: SOA queries sometimes timeout or return incorrect data, possibly related to incomplete SOA record serialization.
**Implementation Plan**:
1. Implement proper SOA record serialization in `serializeRData` method
2. Ensure all SOA fields are properly encoded in wire format
3. Add comprehensive SOA record tests
4. Verify SOA responses with standard DNS tools (dig, nslookup)
**Code Changes**:
- Implement SOA serialization in `serializeRData` method
- Add SOA-specific test cases
#### 3. Configurable DNSSEC Zone Prefix
**Status**: Not Started
**Priority**: Low
**Issue**: The server hardcodes 'ns1.' prefix for SOA mname field which may not match actual nameserver names.
**Implementation Plan**:
1. Add `primaryNameserver` option to `IDnsServerOptions`
2. Default to `ns1.{dnssecZone}` if not provided
3. Update SOA record generation to use configurable nameserver
4. Update documentation with new option
5. Add tests for custom primary nameserver configuration
**Code Changes**:
- Add `primaryNameserver?: string` to `IDnsServerOptions`
- Update SOA mname field generation logic
- Update constructor to handle the new option
### Testing Recommendations
- Test DNSSEC validation with multiple records using `dig +dnssec`
- Verify SOA records with `dig SOA`
- Test custom nameserver configuration
- Validate with real-world DNS resolvers (Google DNS, Cloudflare)

123
test/example.primaryns.ts Normal file
View File

@ -0,0 +1,123 @@
import * as smartdns from '../ts_server/index.js';
// Example: Using custom primary nameserver
async function exampleCustomNameserver() {
const dnsServer = new smartdns.DnsServer({
httpsKey: 'your-https-key',
httpsCert: 'your-https-cert',
httpsPort: 8443,
udpPort: 8053,
dnssecZone: 'example.com',
// Custom primary nameserver for SOA records
primaryNameserver: 'ns-primary.example.com',
});
// Register some handlers
dnsServer.registerHandler('example.com', ['NS'], (question) => {
return {
name: question.name,
type: 'NS',
class: 'IN',
ttl: 3600,
data: 'ns-primary.example.com',
};
});
dnsServer.registerHandler('example.com', ['NS'], (question) => {
return {
name: question.name,
type: 'NS',
class: 'IN',
ttl: 3600,
data: 'ns-secondary.example.com',
};
});
await dnsServer.start();
console.log('DNS server started with custom primary nameserver');
// SOA records will now use 'ns-primary.example.com' instead of 'ns1.example.com'
}
// Example: DNSSEC with multiple records (proper RRset signing)
async function exampleDnssecMultipleRecords() {
const dnsServer = new smartdns.DnsServer({
httpsKey: 'your-https-key',
httpsCert: 'your-https-cert',
httpsPort: 8443,
udpPort: 8053,
dnssecZone: 'secure.example.com',
});
// Register multiple A records for round-robin
const ips = ['192.168.1.10', '192.168.1.11', '192.168.1.12'];
for (const ip of ips) {
dnsServer.registerHandler('www.secure.example.com', ['A'], (question) => {
return {
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: ip,
};
});
}
await dnsServer.start();
console.log('DNS server started with DNSSEC and multiple A records');
// When queried with DNSSEC enabled, all 3 A records will be signed together
// as a single RRset with one RRSIG record (not 3 separate RRSIGs)
}
// Example: Multiple TXT records for various purposes
async function exampleMultipleTxtRecords() {
const dnsServer = new smartdns.DnsServer({
httpsKey: 'your-https-key',
httpsCert: 'your-https-cert',
httpsPort: 8443,
udpPort: 8053,
dnssecZone: 'example.com',
});
// SPF record
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['v=spf1 include:_spf.google.com ~all'],
};
});
// DKIM record
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4...'],
};
});
// Domain verification
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['google-site-verification=1234567890abcdef'],
};
});
await dnsServer.start();
console.log('DNS server started with multiple TXT records');
// All TXT records will be returned when queried
}
// Export examples for reference
export { exampleCustomNameserver, exampleDnssecMultipleRecords, exampleMultipleTxtRecords };

373
test/test.dnssec.rrset.ts Normal file
View File

@ -0,0 +1,373 @@
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 = 8500;
let nextUdpPort = 8501;
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('DNSSEC should sign entire RRset together, not individual 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',
};
});
dnsServer.registerHandler('example.com', ['NS'], (question) => {
return {
name: question.name,
type: 'NS',
class: 'IN',
ttl: 3600,
data: 'ns3.example.com',
};
});
await dnsServer.start();
const client = dgram.createSocket('udp4');
// Create query with DNSSEC requested
const query = dnsPacket.encode({
type: 'query',
id: 1,
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<dnsPacket.Packet>((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;
// Count NS and RRSIG records
const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS');
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
console.log('NS records returned:', nsAnswers.length);
console.log('RRSIG records returned:', rrsigAnswers.length);
// Should have 3 NS records and only 1 RRSIG for the entire RRset
expect(nsAnswers.length).toEqual(3);
expect(rrsigAnswers.length).toEqual(1);
// Verify RRSIG covers NS type
const rrsigData = (rrsigAnswers[0] as any).data;
expect(rrsigData.typeCovered).toEqual('NS');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('SOA records should be properly serialized and returned', 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 subdomain to trigger SOA response
const query = dnsPacket.encode({
type: 'query',
id: 2,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'nonexistent.example.com',
type: 'A',
class: 'IN',
},
],
});
const responsePromise = new Promise<dnsPacket.Packet>((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;
// Should have SOA record in response
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
expect(soaAnswers.length).toEqual(1);
const soaData = (soaAnswers[0] as any).data;
console.log('SOA record:', soaData);
expect(soaData.mname).toEqual('ns1.example.com');
expect(soaData.rname).toEqual('hostmaster.example.com');
expect(typeof soaData.serial).toEqual('number');
expect(soaData.refresh).toEqual(3600);
expect(soaData.retry).toEqual(600);
expect(soaData.expire).toEqual(604800);
expect(soaData.minimum).toEqual(86400);
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('Primary nameserver should be configurable', 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: 'custom-ns.example.com',
});
await dnsServer.start();
const client = dgram.createSocket('udp4');
// Query for SOA record
const query = dnsPacket.encode({
type: 'query',
id: 3,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'example.com',
type: 'SOA',
class: 'IN',
},
],
});
const responsePromise = new Promise<dnsPacket.Packet>((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;
// Should have SOA record with custom nameserver
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
expect(soaAnswers.length).toEqual(1);
const soaData = (soaAnswers[0] as any).data;
console.log('SOA mname:', soaData.mname);
// Should use the custom primary nameserver
expect(soaData.mname).toEqual('custom-ns.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('Multiple A records should have single RRSIG when DNSSEC is enabled', 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 records for round-robin
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) => {
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: 4,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'www.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 responsePromise = new Promise<dnsPacket.Packet>((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');
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
console.log('A records:', aAnswers.length);
console.log('RRSIG records:', rrsigAnswers.length);
// Should have 3 A records and only 1 RRSIG
expect(aAnswers.length).toEqual(3);
expect(rrsigAnswers.length).toEqual(1);
await stopServer(dnsServer);
dnsServer = null;
});
export default tap.start();

228
test/test.fixes.simple.ts Normal file
View File

@ -0,0 +1,228 @@
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 = 8600;
let nextUdpPort = 8601;
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 should be returned for non-existent domains', 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: 1,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'nonexistent.example.com',
type: 'A',
class: 'IN',
},
],
});
const responsePromise = new Promise<dnsPacket.Packet>((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('✅ SOA response 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 mname:', soaData.mname);
console.log('✅ SOA rname:', soaData.rname);
expect(soaData.mname).toEqual('ns1.example.com');
expect(soaData.rname).toEqual('hostmaster.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('Primary nameserver should be configurable', 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: 'custom-ns.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',
},
],
});
const responsePromise = new Promise<dnsPacket.Packet>((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 soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
expect(soaAnswers.length).toEqual(1);
const soaData = (soaAnswers[0] as any).data;
console.log('✅ Custom primary nameserver:', soaData.mname);
expect(soaData.mname).toEqual('custom-ns.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('Default primary nameserver with FQDN', 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.', // FQDN with trailing dot
});
await dnsServer.start();
const client = dgram.createSocket('udp4');
const query = dnsPacket.encode({
type: 'query',
id: 3,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'nonexistent.example.com',
type: 'A',
class: 'IN',
},
],
});
const responsePromise = new Promise<dnsPacket.Packet>((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 soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
const soaData = (soaAnswers[0] as any).data;
console.log('✅ FQDN primary nameserver:', soaData.mname);
expect(soaData.mname).toEqual('ns.example.com.');
await stopServer(dnsServer);
dnsServer = null;
});
export default tap.start();

View File

@ -13,6 +13,8 @@ export interface IDnsServerOptions {
// New options for independent manual socket control
manualUdpMode?: boolean;
manualHttpsMode?: boolean;
// Primary nameserver for SOA records (defaults to ns1.{dnssecZone})
primaryNameserver?: string;
}
export interface DnsAnswer {
@ -559,11 +561,15 @@ export class DnsServer {
};
const dnssecRequested = this.isDnssecRequested(request);
// Map to group records by type for proper DNSSEC RRset signing
const rrsetMap = new Map<string, DnsAnswer[]>();
for (const question of request.questions) {
console.log(`Query for ${question.name} of type ${question.type}`);
let answered = false;
const recordsForQuestion: DnsAnswer[] = [];
// Handle DNSKEY queries if DNSSEC is requested
if (dnssecRequested && question.type === 'DNSKEY' && question.name === this.options.dnssecZone) {
@ -574,40 +580,41 @@ export class DnsServer {
ttl: 3600,
data: this.dnskeyRecord,
};
response.answers.push(dnskeyAnswer as plugins.dnsPacket.Answer);
// Sign the DNSKEY RRset
const rrsig = this.generateRRSIG('DNSKEY', [dnskeyAnswer], question.name);
response.answers.push(rrsig as plugins.dnsPacket.Answer);
recordsForQuestion.push(dnskeyAnswer);
answered = true;
continue;
} else {
// Collect all matching records from handlers
for (const handlerEntry of this.handlers) {
if (
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
handlerEntry.recordTypes.includes(question.type)
) {
const answer = handlerEntry.handler(question);
if (answer) {
// Ensure the answer has ttl and class
const dnsAnswer: DnsAnswer = {
...answer,
ttl: answer.ttl || 300,
class: answer.class || 'IN',
};
recordsForQuestion.push(dnsAnswer);
answered = true;
// Continue processing other handlers to allow multiple records
}
}
}
}
for (const handlerEntry of this.handlers) {
if (
plugins.minimatch.minimatch(question.name, handlerEntry.domainPattern) &&
handlerEntry.recordTypes.includes(question.type)
) {
const answer = handlerEntry.handler(question);
if (answer) {
// Ensure the answer has ttl and class
const dnsAnswer: DnsAnswer = {
...answer,
ttl: answer.ttl || 300,
class: answer.class || 'IN',
};
response.answers.push(dnsAnswer as plugins.dnsPacket.Answer);
if (dnssecRequested) {
// Sign the answer RRset
const rrsig = this.generateRRSIG(question.type, [dnsAnswer], question.name);
response.answers.push(rrsig as plugins.dnsPacket.Answer);
}
answered = true;
// Continue processing other handlers to allow multiple records
}
// Add records to response and group by type for DNSSEC
if (recordsForQuestion.length > 0) {
for (const record of recordsForQuestion) {
response.answers.push(record as plugins.dnsPacket.Answer);
}
// Group records by type for DNSSEC signing
if (dnssecRequested) {
const rrsetKey = `${question.name}:${question.type}`;
rrsetMap.set(rrsetKey, recordsForQuestion);
}
}
@ -620,7 +627,7 @@ export class DnsServer {
class: 'IN',
ttl: 3600,
data: {
mname: `ns1.${this.options.dnssecZone}`,
mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
rname: `hostmaster.${this.options.dnssecZone}`,
serial: Math.floor(Date.now() / 1000),
refresh: 3600,
@ -633,6 +640,16 @@ export class DnsServer {
}
}
// Sign RRsets if DNSSEC is requested
if (dnssecRequested) {
for (const [key, rrset] of rrsetMap) {
const [name, type] = key.split(':');
// Sign the entire RRset together
const rrsig = this.generateRRSIG(type, rrset, name);
response.answers.push(rrsig as plugins.dnsPacket.Answer);
}
}
return response;
}
@ -760,9 +777,21 @@ export class DnsServer {
// NS records contain domain names
return this.nameToBuffer(data);
case 'SOA':
// Implement SOA record serialization if needed
// For now, return an empty buffer or handle as needed
return Buffer.alloc(0);
// Implement SOA record serialization according to RFC 1035
const mname = this.nameToBuffer(data.mname);
const rname = this.nameToBuffer(data.rname);
const serial = Buffer.alloc(4);
serial.writeUInt32BE(data.serial, 0);
const refresh = Buffer.alloc(4);
refresh.writeUInt32BE(data.refresh, 0);
const retry = Buffer.alloc(4);
retry.writeUInt32BE(data.retry, 0);
const expire = Buffer.alloc(4);
expire.writeUInt32BE(data.expire, 0);
const minimum = Buffer.alloc(4);
minimum.writeUInt32BE(data.minimum, 0);
return Buffer.concat([mname, rname, serial, refresh, retry, expire, minimum]);
// Add cases for other record types as needed
default:
throw new Error(`Serialization for record type ${type} is not implemented.`);