Building a simple http server to query dns server

Ronit PandaRonit Panda
4 min read

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.

  1. 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';
      
  2. 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;
      
  3. Creating the Root Route:

    • Define a root route that responds with a simple "Hello World" message.

        app.get('/', (_req, res) => {
            res.send('Hello World');
        });
      
  4. 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 url

        const resolveSchema = z.object({
            domain: z.string().refine(isValidDomain, {
                message: 'Invalid domain',
            }),
            type: z.nativeEnum(RECORD_TYPE_STRING),
        });
        type ResolveSchema = z.infer<typeof resolveSchema>;
      
  5. 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');
                }
            },
        );
      
  6. 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

  1. Will run both the udp server and http server on dev mode with hot reload

     pnpm run dev
    
  2. open up another terminal and run

     curl -X GET "http://localhost:8080/resolve?domain=google.com&type=A"
    
  3. 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.

0
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