Compare commits

...

5 Commits

Author SHA1 Message Date
1811ebd4d4 7.4.6 2025-05-30 19:28:54 +00:00
e7ace9b596 7.4.5 2025-05-30 19:28:22 +00:00
f6175d1f2b 7.4.4
fix(dnsserver): Fix SOA record timeout issue by correcting RRSIG field formatting

- Fixed RRSIG generation by using correct field name 'signersName' (not 'signerName')
- Fixed label count calculation in RRSIG by filtering empty strings
- Added SOA records to DNSSEC signing map for proper RRSIG generation
- Added error logging and fallback values for RRSIG generation robustness
- Updated test expectations to match corrected DNSSEC RRset signing behavior
- Added comprehensive SOA test coverage including timeout, debug, and simple test scenarios
2025-05-30 19:27:37 +00:00
d67fbc87e2 7.4.3 2025-05-30 18:27:28 +00:00
b87cbbee5c 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>
2025-05-30 18:20:55 +00:00
13 changed files with 1869 additions and 53 deletions

View File

@ -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.

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartdns",
"version": "7.4.2",
"version": "7.4.6",
"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": {

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

@ -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;

269
test/test.soa.debug.ts Normal file
View File

@ -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<dnsPacket.Packet>((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<dnsPacket.Packet>((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();

271
test/test.soa.final.ts Normal file
View File

@ -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<dnsPacket.Packet>((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<dnsPacket.Packet>((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<dnsPacket.Packet>((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<dnsPacket.Packet>((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();

201
test/test.soa.simple.ts Normal file
View File

@ -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<dnsPacket.Packet>((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<dnsPacket.Packet>((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();

224
test/test.soa.timeout.ts Normal file
View File

@ -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<dnsPacket.Packet>((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();

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 {
@ -560,10 +562,14 @@ 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,16 +580,10 @@ 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) &&
@ -597,19 +597,26 @@ export class DnsServer {
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);
}
recordsForQuestion.push(dnsAnswer);
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);
}
}
if (!answered) {
console.log(`No handler found for ${question.name} of type ${question.type}`);
@ -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,
@ -630,6 +637,22 @@ 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]);
}
}
}
// 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);
}
}
@ -668,6 +691,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 = {
name,
@ -675,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
},
};
@ -760,9 +794,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.`);