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