Testing the recursive resolver
This will be very straightforward.
So without any delay head over to src/udp-server.test.ts
Initial setup
Add all required imports
Start up a udpSocket on udp4
TEST_PORT & TEST_HOST signify the host and port on which our actual udp server is running, onto which we need to send DNS queries to get response back
Make sure before running these tests to run the UDP server by running
pnpm run dev
Lastly, the afterAll block ensures the udpSocket is closed after all the tests in this file have run.
import * as dgram from 'node:dgram'; import { DNSBuilder } from './message/builder'; import { dnsParser } from './message/parser'; import { Bool, DNSObject, OPCODE, QRIndicator, RCode, RECORD_TYPE, } from './message/types'; import { decodeRDATA } from './utils'; const udpSocket = dgram.createSocket('udp4'); const TEST_PORT = 2053; const TEST_HOST = '127.0.0.1'; afterAll((done) => { udpSocket.close(done); });
Test 1
if you look into the test it's fairly simple
Create a dns object with 1 question and request for an A record
Convert the object to a buffer, and send it via UDP to our dns server
On response (message event) we parse the result and fetch the answers of it, expecting it to be valid and containing the answer that we expect
test('valid DNS request with A record and sigle question', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'google.com',
TYPE: RECORD_TYPE.A,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const { answers } = dnsParser.parse(data);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[0]?.CLASS).toEqual(1);
expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.A);
expect(answers[0]?.RDLENGTH).toEqual(4);
expect(answers[0]?.RDATA).toBeDefined();
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
It will be a waste of time if we go over all tests, given each test follows the same template as above, we are varying the record type and number of records etc to test as much as possible
So am attaching the whole test file below (src/udp-server.test.ts
) and you can just take a short glance over all of them
import * as dgram from 'node:dgram';
import { DNSBuilder } from './message/builder';
import { dnsParser } from './message/parser';
import {
Bool,
DNSObject,
OPCODE,
QRIndicator,
RCode,
RECORD_TYPE,
} from './message/types';
import { decodeRDATA } from './utils';
const udpSocket = dgram.createSocket('udp4');
const TEST_PORT = 2053;
const TEST_HOST = '127.0.0.1';
afterAll((done) => {
udpSocket.close(done);
});
test('valid DNS request with A record and sigle question', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'google.com',
TYPE: RECORD_TYPE.A,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const { answers } = dnsParser.parse(data);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[0]?.CLASS).toEqual(1);
expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.A);
expect(answers[0]?.RDLENGTH).toEqual(4);
expect(answers[0]?.RDATA).toBeDefined();
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
test('valid DNS request with CNAME record and sigle question should resolve underlying A record if asked', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'www.ronit.dev',
TYPE: RECORD_TYPE.A,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const { answers } = dnsParser.parse(data);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers).toBeDefined();
expect(answers.length).toEqual(2);
expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[0]?.CLASS).toEqual(1);
expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.CNAME);
expect(answers[0]?.RDLENGTH).toEqual(18);
expect(answers[1]?.NAME).toEqual('hashnode.network');
expect(answers[1]?.CLASS).toEqual(1);
expect(answers[1]?.TYPE).toEqual(RECORD_TYPE.A);
expect(answers[1]?.RDLENGTH).toEqual(4);
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
test('valid DNS request with CNAME record explicit asking of CNAME record should resolve only that', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'www.ronit.dev',
TYPE: RECORD_TYPE.CNAME,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const { answers } = dnsParser.parse(data);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers.length).toEqual(1);
expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[0]?.CLASS).toEqual(1);
expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.CNAME);
expect(answers[0]?.RDLENGTH).toEqual(18);
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
test('invalid domain should give an NXDOMAIN rcode in header', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'lambda.ronit.dev',
TYPE: RECORD_TYPE.A,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const {
header: { RCODE, ANCOUNT, NSCOUNT },
} = dnsParser.parse(data);
expect(RCODE).toEqual(RCode.NXDOMAIN);
expect(ANCOUNT).toEqual(0);
expect(NSCOUNT).toEqual(1);
const { authority, answers } = dnsParser.parse(data);
expect(authority).toBeDefined();
if (!authority) return;
expect(authority).toBeDefined();
expect(authority.length).toEqual(1);
expect(authority[0]?.NAME).toEqual('ronit.dev');
expect(authority[0]?.CLASS).toEqual(1);
expect(authority[0]?.TYPE).toEqual(RECORD_TYPE.SOA);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers.length).toEqual(0);
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
test('valid DNS request with NS record', async () => {
const dnsRequestObject: DNSObject = {
header: {
ID: 1234,
QR: QRIndicator.QUERY,
OPCODE: OPCODE.QUERY,
AA: Bool.FALSE,
TC: Bool.FALSE,
RD: Bool.TRUE,
RA: Bool.FALSE,
Z: 0,
RCODE: RCode.NOERROR,
QDCOUNT: 1,
ANCOUNT: 0,
NSCOUNT: 0,
ARCOUNT: 0,
},
questions: [
{
NAME: 'ronit.dev',
TYPE: RECORD_TYPE.NS,
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
// Setup a promise to wait for the UDP response
const responsePromise = new Promise<void>((resolve, reject) => {
udpSocket.send(requestBuffer, TEST_PORT, TEST_HOST, (err) => {
if (err) {
reject(err);
}
});
udpSocket.on('message', (data: Buffer) => {
try {
const { answers } = dnsParser.parse(data);
expect(answers).toBeDefined();
if (!answers) return;
expect(answers.length).toEqual(2);
expect(answers[0]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[0]?.CLASS).toEqual(1);
expect(answers[0]?.TYPE).toEqual(RECORD_TYPE.NS);
expect(answers[0]?.RDLENGTH).toEqual(24);
expect(answers[0]?.RDATA).toBeDefined();
expect(decodeRDATA(answers[0]?.RDATA as Buffer)).toEqual(
'greg.ns.cloudflare.com',
);
expect(answers[1]?.NAME).toEqual(dnsRequestObject.questions[0]?.NAME);
expect(answers[1]?.CLASS).toEqual(1);
expect(answers[1]?.TYPE).toEqual(RECORD_TYPE.NS);
expect(answers[1]?.RDLENGTH).toEqual(8);
expect(answers[1]?.RDATA).toBeDefined();
expect(decodeRDATA(answers[1]?.RDATA as Buffer)).toEqual('khloe'); // this is due to compression in the response
resolve(); // Resolve the promise when all assertions pass
} catch (error) {
reject(error); // Reject with error if assertions fail
}
});
});
// Wait for the response promise to resolve or timeout after a certain period
await responsePromise;
});
To run this test simply run:
pnpm run test
As said earlier make sure the UDP server is actually running on the given host and port before running the tests
pnpm run dev
if the test passes means we our classes are working just fine,
& trust me they will ๐
Now let's move to the next blog. Where we will be implementing the dns cache class to make requests to our UDP server faster.
Subscribe to my newsletter
Read articles from Ronit Panda directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ronit Panda
Ronit Panda
Founding full stack engineer at dimension.dev