Files
smartdns/test/test.soa.debug.ts

361 lines
9.5 KiB
TypeScript
Raw Normal View History

import * as plugins from '../ts_server/plugins.js';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
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');
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
console.log('SOA records found:', soaAnswers.length);
console.log('RRSIG records found:', rrsigAnswers.length);
// Must have exactly 1 SOA for the zone
expect(soaAnswers.length).toEqual(1);
// Must have at least 1 RRSIG covering the SOA
expect(rrsigAnswers.length).toBeGreaterThan(0);
// Verify RRSIG covers SOA type
const rrsigData = (rrsigAnswers[0] as any).data;
expect(rrsigData.typeCovered).toEqual('SOA');
// Verify SOA data fields are present and valid
const soaData = (soaAnswers[0] as any).data;
console.log('SOA data:', soaData);
expect(soaData.mname).toStartWith('ns'); // nameserver
expect(soaData.rname).toInclude('.'); // responsible party email
expect(typeof soaData.serial).toEqual('number');
expect(soaData.refresh).toBeGreaterThan(0);
expect(soaData.retry).toBeGreaterThan(0);
expect(soaData.expire).toBeGreaterThan(0);
expect(soaData.minimum).toBeGreaterThan(0);
} catch (error) {
console.error('SOA query with DNSSEC failed:', error);
throw error;
}
await stopServer(dnsServer);
dnsServer = null;
});
tap.test('SOA serialization produces correct wire format', async () => {
const httpsData = await tapNodeTools.createHttpsCert();
const udpPort = getUniqueUdpPort();
dnsServer = new smartdns.DnsServer({
httpsKey: httpsData.key,
httpsCert: httpsData.cert,
httpsPort: getUniqueHttpsPort(),
udpPort: udpPort,
dnssecZone: 'roundtrip.example.com',
});
// Register a handler with specific SOA data we can verify round-trips correctly
const expectedSoa = {
mname: 'ns1.roundtrip.example.com',
rname: 'admin.roundtrip.example.com',
serial: 2025020101,
refresh: 7200,
retry: 1800,
expire: 1209600,
minimum: 43200,
};
dnsServer.registerHandler('roundtrip.example.com', ['SOA'], (question) => {
return {
name: question.name,
type: 'SOA',
class: 'IN',
ttl: 3600,
data: expectedSoa,
};
});
await dnsServer.start();
const client = dgram.createSocket('udp4');
// Plain UDP query without DNSSEC to test pure SOA serialization
const query = dnsPacket.encode({
type: 'query',
id: 3,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
name: 'roundtrip.example.com',
type: 'SOA',
class: 'IN',
},
],
});
console.log('Sending plain SOA query for serialization round-trip test');
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;
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
expect(soaAnswers.length).toEqual(1);
const soaData = (soaAnswers[0] as any).data;
console.log('Round-trip SOA data:', soaData);
// Verify all 7 SOA fields survived the full round-trip:
// handler → Rust encode_soa → wire → dns-packet decode
expect(soaData.mname).toEqual(expectedSoa.mname);
expect(soaData.rname).toEqual(expectedSoa.rname);
expect(soaData.serial).toEqual(expectedSoa.serial);
expect(soaData.refresh).toEqual(expectedSoa.refresh);
expect(soaData.retry).toEqual(expectedSoa.retry);
expect(soaData.expire).toEqual(expectedSoa.expire);
expect(soaData.minimum).toEqual(expectedSoa.minimum);
} catch (error) {
console.error('SOA serialization round-trip test failed:', error);
throw error;
}
await stopServer(dnsServer);
dnsServer = null;
});
export default tap.start();