Building a simple http server to query dns server
We are almost at the end, in this blog we will be building a simple http server which will resolve our DNS queries by querying the underlying UDP server we built, providing us with an easy to use interface.
Honestly we are just going to setup a bare bones express server, with one api route. And write some really simple code to resolve dns queries
head over to src/http-server.ts
import express, { Request, Response } from 'express';
import { z } from 'zod';
import { forwardResolver } from './forward-resolver';
import { DNSBuilder } from './message/builder';
import {
Bool,
DNSObject,
OPCODE,
QRIndicator,
RCode,
RECORD_TYPE,
RECORD_TYPE_STRING,
} from './message/types';
import { isValidDomain } from './utils';
const app = express();
const DNS_SERVER_HOST = '127.0.0.1';
const DNS_SERVER_PORT = 2053;
app.get('/', (_req, res) => {
res.send('Hello World');
});
const resolveSchema = z.object({
domain: z.string().refine(isValidDomain, {
message: 'Invalid domain',
}),
type: z.nativeEnum(RECORD_TYPE_STRING),
});
type ResolveSchema = z.infer<typeof resolveSchema>;
app.get(
'/resolve',
async (req: Request<{}, {}, {}, ResolveSchema>, res: Response) => {
try {
const parsed = resolveSchema.safeParse(req.query);
if (!parsed.success) {
console.error(parsed.error);
return res.status(400).send(parsed.error.message);
}
const { domain, type } = req.query;
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: domain,
TYPE: RECORD_TYPE[type],
CLASS: 1,
},
],
};
const dnsBuilder = new DNSBuilder(dnsRequestObject);
const requestBuffer = dnsBuilder.toBuffer();
const response = await forwardResolver(
requestBuffer,
DNS_SERVER_HOST,
DNS_SERVER_PORT,
);
res.json(response);
} catch (error) {
console.error(error);
res.status(500).send('Internal server error');
}
},
);
app.listen(8080, () => {
console.log('Server is running on port 8080');
});
Now as usual let's break it down line by line.
Importing Necessary Modules:
Import the required modules (
express
,zod
,forward-resolver
, and utility functions).import express, { Request, Response } from 'express'; import { z } from 'zod'; import { forwardResolver } from './forward-resolver'; import { DNSBuilder } from './message/builder'; import { Bool, DNSObject, OPCODE, QRIndicator, RCode, RECORD_TYPE, RECORD_TYPE_STRING, } from './message/types'; import { isValidDomain } from './utils';
Setting Up Express Application:
Create an Express application instance.
Define constants for the DNS server host and port, that we built over the blogs.
const app = express(); const DNS_SERVER_HOST = '127.0.0.1'; const DNS_SERVER_PORT = 2053;
Creating the Root Route:
Define a root route that responds with a simple "Hello World" message.
app.get('/', (_req, res) => { res.send('Hello World'); });
Defining the Resolve Schema with Zod:
Create a Zod schema to validate query parameters for the
/resolve
endpoint.User's have to simply make a get request like
http://localhost:8080/resolve?domain=example.com&type=1
this urlconst resolveSchema = z.object({ domain: z.string().refine(isValidDomain, { message: 'Invalid domain', }), type: z.nativeEnum(RECORD_TYPE_STRING), }); type ResolveSchema = z.infer<typeof resolveSchema>;
Creating the Resolve Route:
Define a
/resolve
route to handle DNS resolution requests.Validate query parameters using the Zod schema.
Construct a DNS request object and forward it to the UDP server.
Return the DNS response to the client.
We are used to all these functions so let's not go deep into all of them, if you want a recap or have come directly to this blog, please read through the whole thing
app.get( '/resolve', async (req: Request<{}, {}, {}, ResolveSchema>, res: Response) => { try { const parsed = resolveSchema.safeParse(req.query); if (!parsed.success) { console.error(parsed.error); return res.status(400).send(parsed.error.message); } const { domain, type } = req.query; 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: domain, TYPE: RECORD_TYPE[type], CLASS: 1, }, ], }; const dnsBuilder = new DNSBuilder(dnsRequestObject); const requestBuffer = dnsBuilder.toBuffer(); const response = await forwardResolver( requestBuffer, DNS_SERVER_HOST, DNS_SERVER_PORT, ); res.json(response); } catch (error) { console.error(error); res.status(500).send('Internal server error'); } }, );
Starting the Server:
Start the Express server on port 8080.
app.listen(8080, () => { console.log('Server is running on port 8080'); });
Now finally we are all done, to test if this is working or not, just run
Will run both the udp server and http server on dev mode with hot reload
pnpm run dev
open up another terminal and run
curl -X GET "http://localhost:8080/resolve?domain=google.com&type=A"
You can open this URL and manipulate with various requests on your browser and you will get a response like this
{ "questions": [ { "NAME": "www.ronit.dev", "TYPE": 1, "CLASS": 1 } ], "answers": [ { "NAME": "www.ronit.dev", "TYPE": 5, "CLASS": 1, "TTL": 300, "RDLENGTH": 18, "RDATA": { "type": "Buffer", "data": [ 8, 104, 97, 115, 104, 110, 111, 100, 101, 7, 110, 101, 116, 119, 111, 114, 107, 0 ] } }, { "NAME": "hashnode.network", "TYPE": 1, "CLASS": 1, "TTL": 300, "RDLENGTH": 4, "RDATA": { "type": "Buffer", "data": [76, 76, 21, 21] } } ], "authority": [], "additional": [], "header": { "ID": 1234, "QR": 1, "OPCODE": 0, "AA": 0, "TC": 0, "RD": 1, "RA": 1, "Z": 0, "RCODE": 0, "QDCOUNT": 1, "ANCOUNT": 2, "NSCOUNT": 0, "ARCOUNT": 0 } }
With that, we conclude our blog series on building a fully functional prototype DNS server; see you in the next series where we'll create something interesting from scratch.
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