237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
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;
|
|
|
|
// 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 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);
|
|
|
|
// 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');
|
|
} catch (error) {
|
|
console.error('DNSSEC SOA query failed:', error);
|
|
throw error;
|
|
}
|
|
|
|
await stopServer(dnsServer);
|
|
dnsServer = null;
|
|
});
|
|
|
|
tap.test('DNSSEC signing completes within reasonable time', async () => {
|
|
const httpsData = await tapNodeTools.createHttpsCert();
|
|
const udpPort = 8756;
|
|
|
|
dnsServer = new smartdns.DnsServer({
|
|
httpsKey: httpsData.key,
|
|
httpsCert: httpsData.cert,
|
|
httpsPort: 8757,
|
|
udpPort: udpPort,
|
|
dnssecZone: 'perf.example.com',
|
|
});
|
|
|
|
// No handlers registered — server returns SOA for nonexistent domain
|
|
await dnsServer.start();
|
|
|
|
const client = dgram.createSocket('udp4');
|
|
|
|
const query = dnsPacket.encode({
|
|
type: 'query',
|
|
id: 2,
|
|
flags: dnsPacket.RECURSION_DESIRED,
|
|
questions: [
|
|
{
|
|
name: 'nonexistent.perf.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 performance test...');
|
|
|
|
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 — exceeds 2s budget`));
|
|
}, 2000);
|
|
|
|
client.on('message', (msg) => {
|
|
clearTimeout(timeout);
|
|
const elapsed = Date.now() - startTime;
|
|
console.log(`DNSSEC 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);
|
|
reject(err);
|
|
client.close();
|
|
});
|
|
|
|
client.send(query, udpPort, 'localhost', (err) => {
|
|
if (err) {
|
|
clearTimeout(timeout);
|
|
reject(err);
|
|
client.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
try {
|
|
const dnsResponse = await responsePromise;
|
|
const elapsed = Date.now() - startTime;
|
|
|
|
// Response must arrive within 2 seconds (generous for CI)
|
|
expect(elapsed).toBeLessThan(2000);
|
|
|
|
// Verify correctness: SOA + RRSIG present
|
|
const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA');
|
|
const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG');
|
|
|
|
expect(soaAnswers.length).toEqual(1);
|
|
expect(rrsigAnswers.length).toBeGreaterThan(0);
|
|
|
|
const rrsigData = (rrsigAnswers[0] as any).data;
|
|
expect(rrsigData.typeCovered).toEqual('SOA');
|
|
|
|
console.log(`DNSSEC signing performance OK: ${elapsed}ms`);
|
|
} catch (error) {
|
|
console.error('DNSSEC performance test failed:', error);
|
|
throw error;
|
|
}
|
|
|
|
await stopServer(dnsServer);
|
|
dnsServer = null;
|
|
});
|
|
|
|
export default tap.start(); |