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.

This commit is contained in:
2025-05-30 17:09:02 +00:00
parent e7cb0921fc
commit 2b97dffb47
8 changed files with 1290 additions and 89 deletions

View File

@ -0,0 +1,485 @@
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, each NS record should have an associated RRSIG
expect(nsAnswers.length).toEqual(2);
expect(rrsigAnswers.length).toEqual(2);
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();

View File

@ -0,0 +1,279 @@
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 = 8400;
let nextUdpPort = 8401;
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('Multiple NS records should work 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: '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');
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('✅ NS records returned:', dnsResponse.answers.length);
console.log('✅ NS records:', dnsResponse.answers.map(a => (a as any).data));
// SUCCESS: Multiple NS records are now returned
expect(dnsResponse.answers.length).toEqual(2);
expect(dnsResponse.answers.map(a => (a as any).data).sort()).toEqual(['ns1.example.com', 'ns2.example.com']);
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('Multiple A records for round-robin DNS', 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
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: 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('✅ A records returned:', dnsResponse.answers.length);
console.log('✅ A records:', dnsResponse.answers.map(a => (a as any).data));
// SUCCESS: All A records for round-robin DNS
expect(dnsResponse.answers.length).toEqual(3);
expect(dnsResponse.answers.map(a => (a as any).data).sort()).toEqual(['10.0.0.1', '10.0.0.2', '10.0.0.3']);
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('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 records
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) => {
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('✅ TXT records returned:', dnsResponse.answers.length);
// SUCCESS: All TXT records are returned
expect(dnsResponse.answers.length).toEqual(3);
const txtData = dnsResponse.answers.map(a => (a as any).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;
});
export default tap.start();

View File

@ -0,0 +1,419 @@
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 = 8200;
let nextUdpPort = 8201;
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 demonstrate the current limitation with multiple NS 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 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('Current behavior - NS records returned:', dnsResponse.answers.length);
console.log('NS records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 NS record due to the break statement
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data).toEqual('ns1.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should demonstrate the limitation with multiple A records (round-robin)', 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
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
console.log('First A handler called');
return {
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: '10.0.0.1',
};
});
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
console.log('Second A handler called');
return {
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: '10.0.0.2',
};
});
dnsServer.registerHandler('www.example.com', ['A'], (question) => {
console.log('Third A handler called');
return {
name: question.name,
type: 'A',
class: 'IN',
ttl: 300,
data: '10.0.0.3',
};
});
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('Current behavior - A records returned:', dnsResponse.answers.length);
console.log('A records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 A record, preventing round-robin DNS
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data).toEqual('10.0.0.1');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should demonstrate the limitation with 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
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
console.log('SPF handler called');
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['v=spf1 include:_spf.example.com ~all'],
};
});
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
console.log('DKIM handler called');
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNA...'],
};
});
dnsServer.registerHandler('example.com', ['TXT'], (question) => {
console.log('Domain verification handler called');
return {
name: question.name,
type: 'TXT',
class: 'IN',
ttl: 3600,
data: ['google-site-verification=1234567890abcdef'],
};
});
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('Current behavior - TXT records returned:', dnsResponse.answers.length);
console.log('TXT records:', dnsResponse.answers.map(a => (a as any).data));
// CURRENT BEHAVIOR: Only returns 1 TXT record instead of all 3
expect(dnsResponse.answers.length).toEqual(1);
expect((dnsResponse.answers[0] as any).data[0]).toInclude('spf1');
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('should show the current workaround pattern', 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',
});
// WORKAROUND: Create an array to store NS records and return them from a single handler
const nsRecords = ['ns1.example.com', 'ns2.example.com'];
let nsIndex = 0;
// This workaround still doesn't solve the problem because only one handler executes
dnsServer.registerHandler('example.com', ['NS'], (question) => {
const record = nsRecords[nsIndex % nsRecords.length];
nsIndex++;
return {
name: question.name,
type: 'NS',
class: 'IN',
ttl: 3600,
data: record,
};
});
await dnsServer.start();
// Make two queries to show the workaround behavior
const client1 = dgram.createSocket('udp4');
const client2 = dgram.createSocket('udp4');
const query = dnsPacket.encode({
type: 'query',
id: 4,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'example.com',
type: 'NS',
class: 'IN',
},
],
});
const responsePromise1 = new Promise<dnsPacket.Packet>((resolve, reject) => {
client1.on('message', (msg) => {
const dnsResponse = dnsPacket.decode(msg);
resolve(dnsResponse);
client1.close();
});
client1.send(query, udpPort, 'localhost');
});
const responsePromise2 = new Promise<dnsPacket.Packet>((resolve, reject) => {
client2.on('message', (msg) => {
const dnsResponse = dnsPacket.decode(msg);
resolve(dnsResponse);
client2.close();
});
setTimeout(() => {
client2.send(query, udpPort, 'localhost');
}, 100);
});
const [response1, response2] = await Promise.all([responsePromise1, responsePromise2]);
console.log('First query NS:', (response1.answers[0] as any).data);
console.log('Second query NS:', (response2.answers[0] as any).data);
// This workaround rotates between records but still only returns one at a time
expect(response1.answers.length).toEqual(1);
expect(response2.answers.length).toEqual(1);
expect((response1.answers[0] as any).data).toEqual('ns1.example.com');
expect((response2.answers[0] as any).data).toEqual('ns2.example.com');
await stopServer(dnsServer);
dnsServer = null;
});
export default tap.start();