npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jnode/dns

v1.0.4

Published

Simple DNS server package for Node.js

Readme

@jnode/dns

Simple DNS packet, client, and UDP server utilities for Node.js.

@jnode/dns provides a small low-level toolkit for reading and writing DNS packets, routing DNS questions, responding from custom handlers, and forwarding queries to upstream DNS resolvers.

README.md is written by Codex, not sure if it understands my code.

Features

  • Parse DNS packets from Buffer objects.
  • Build DNS query and response packets.
  • Run a UDP DNS server with composable routers and handlers.
  • Route requests by label, question type, or custom logic.
  • Proxy requests through an upstream DNS client.
  • Use either CommonJS require() or ESM import.
  • No runtime dependencies outside Node.js built-in modules.

Installation

npm install @jnode/dns

Requirements

  • Node.js with support for node: built-in module specifiers.
  • Permission to bind the port you choose for the DNS server.

DNS commonly uses port 53. On many operating systems, binding to port 53 requires administrator or root privileges. During development, use a high port such as 5353.

Quick Start

Create a UDP DNS Server

const {
    createServer,
    DnsPacket,
    DnsClient,
    routerConstructors: r,
    handlerConstructors: h
} = require('@jnode/dns');

const cloudflare = new DnsClient(DnsClient.SERVERS.CLOUDFLARE);

const registery = {
    localNode: (ctx) => {
        const question = ctx.packet.questions[0];

        ctx.respond({
            answers: [{
                aname: `${question.qname}.`,
                atype: DnsPacket.TYPE.A,
                aclass: question.qclass,
                ttl: 60,
                rdata: Buffer.from([127, 0, 0, 1])
            }]
        });
    },
    default: h.DnsClient(cloudflare),
    error: h.Error(DnsPacket.RCODE.SERVER_FAILURE)
};

const router = r.Label('default', {
    '@ A .node.j': 'localNode',
    '*': 'default'
});

const server = createServer(router, { registery });

server.on('error', console.error);
server.on('warn', console.warn);

server.listen(
    () => console.log('UDP DNS server listening on port 5353'),
    undefined,
    5353
);

Query it with:

dig @127.0.0.1 -p 5353 node.j A
dig @127.0.0.1 -p 5353 example.com A

node.j is answered locally with 127.0.0.1. Other queries fall through to the default handler, which forwards them to Cloudflare.

Send a DNS Query

const { DnsClient, DnsPacket } = require('@jnode/dns');

const client = new DnsClient(DnsClient.SERVERS.CLOUDFLARE);

const packet = new DnsPacket({
    rd: true,
    questions: [{
        qname: 'example.com',
        qtype: DnsPacket.TYPE.A,
        qclass: DnsPacket.CLASS.IN
    }]
});

client.query(packet).then((response) => {
    console.log(response.answers);
}, console.error);

Importing

CommonJS

const {
    DnsPacket,
    DnsClient,
    createServer,
    DnsServer,
    LabelRouter,
    TypeRouter,
    FunctionRouter,
    LabelArgRouter,
    SetRegisteryRouter,
    ErrorHandler,
    PacketHandler,
    RecordHandler,
    DnsClientHandler
} = require('@jnode/dns');

ESM

import {
    DnsPacket,
    DnsClient,
    createServer,
    DnsServer,
    LabelRouter,
    TypeRouter,
    FunctionRouter,
    LabelArgRouter,
    SetRegisteryRouter,
    ErrorHandler,
    PacketHandler,
    RecordHandler,
    DnsClientHandler
} from '@jnode/dns';

API Reference

DnsPacket

DnsPacket represents a DNS message.

const packet = new DnsPacket({
    id: 1234,
    qr: DnsPacket.QR.QUERY,
    opcode: DnsPacket.OPCODE.QUERY,
    aa: false,
    tc: false,
    rd: true,
    ra: false,
    rcode: DnsPacket.RCODE.NO_ERROR_CONDITION,
    questions: [],
    answers: [],
    authorities: [],
    additionals: []
});

Static Constants

DnsPacket.QR

DnsPacket.QR.QUERY
DnsPacket.QR.RESPONSE

DnsPacket.OPCODE

DnsPacket.OPCODE.QUERY
DnsPacket.OPCODE.IQUERY
DnsPacket.OPCODE.STATUS

DnsPacket.RCODE

DnsPacket.RCODE.NO_ERROR_CONDITION
DnsPacket.RCODE.FORMAT_ERROR
DnsPacket.RCODE.SERVER_FAILURE
DnsPacket.RCODE.NAME_ERROR
DnsPacket.RCODE.NOT_IMPLEMENTED
DnsPacket.RCODE.REFUSED

DnsPacket.TYPE

Includes common DNS type codes:

DnsPacket.TYPE.A
DnsPacket.TYPE.NS
DnsPacket.TYPE.CNAME
DnsPacket.TYPE.SOA
DnsPacket.TYPE.PTR
DnsPacket.TYPE.MX
DnsPacket.TYPE.TXT
DnsPacket.TYPE.OPT
DnsPacket.TYPE['*']

The full exported type map also includes legacy and experimental values such as MD, MF, MB, MG, MR, NULL, WKS, HINFO, MINFO, AXFR, MAILB, and MAILA.

DnsPacket.CLASS

DnsPacket.CLASS.IN
DnsPacket.CLASS.CS
DnsPacket.CLASS.CH
DnsPacket.CLASS.HS
DnsPacket.CLASS['*']

DnsPacket.from(buffer)

Parses a DNS packet from a Buffer.

const packet = DnsPacket.from(buffer);

Throws when the packet is shorter than a DNS header or when name compression pointers recurse too deeply.

new DnsPacket(packet)

Creates a packet. Missing fields receive defaults.

Default values:

| Field | Default | | --- | --- | | id | Random 16-bit identifier | | qr | DnsPacket.QR.QUERY | | opcode | DnsPacket.OPCODE.QUERY | | aa | false | | tc | false | | rd | true | | ra | false | | z | 0 | | rcode | DnsPacket.RCODE.NO_ERROR_CONDITION | | questions | [] | | answers | [] | | authorities | [] | | additionals | [] |

packet.toBuffer()

Serializes a packet to a DNS wire-format Buffer.

const buffer = packet.toBuffer();

The serializer currently emits the DNS header, question section, and answer section.

Question Shape

Questions use this object shape:

{
    qname: 'example.com',
    qtype: DnsPacket.TYPE.A,
    qclass: DnsPacket.CLASS.IN
}

Record Shape

Parsed answer, authority, and additional records use this object shape:

{
    aname: 'example.com',
    atype: DnsPacket.TYPE.A,
    aclass: DnsPacket.CLASS.IN,
    ttl: 60,
    rdata: Buffer.from([127, 0, 0, 1])
}

rdata is raw DNS record data. This package does not decode or encode type-specific record payloads for you.

For an A record, rdata is four bytes:

Buffer.from([192, 0, 2, 1])

For many other record types, including CNAME, MX, and TXT, you must encode the record data in DNS wire format yourself.

Server API

createServer(router, options)

Creates a DnsServer.

const server = createServer(router, options);

This is equivalent to:

const server = new DnsServer(router, options);

new DnsServer(router, options)

Creates a DNS server.

const server = new DnsServer(router, {
    maxRoutingSteps: 50,
    registery: {
        default: (ctx) => ctx.respond({ rcode: DnsPacket.RCODE.NOT_IMPLEMENTED }),
        error: (ctx) => ctx.respond({ rcode: DnsPacket.RCODE.SERVER_FAILURE })
    }
});

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | maxRoutingSteps | number | 50 | Maximum number of router transitions before routing stops. | | registery | object | {} | Named handler registry used when a router returns a string. |

The option name is spelled registery in the current API.

server.listen(udpCallback, tcpCallback, port)

Starts the UDP socket and TCP listener on port.

server.listen(
    () => console.log('UDP listening'),
    () => console.log('TCP listening'),
    5353
);

If port is omitted, the server uses port 53.

server.close(udpCallback, tcpCallback)

Closes the UDP socket.

server.close(() => console.log('UDP closed'));

Server Events

DnsServer extends EventEmitter.

| Event | Description | | --- | --- | | udpListening | Emitted when the UDP socket starts listening. | | error | Emitted for UDP socket errors or handler failures. | | warn | Emitted when the error handler itself fails. |

Request Context

Handlers receive ctx and env.

function handler(ctx, env) {
    ctx.respond({ rcode: DnsPacket.RCODE.NO_ERROR_CONDITION });
}

ctx contains:

| Property | Description | | --- | --- | | packet | The parsed incoming DnsPacket. | | respond(packetLike) | Sends a DNS response. Accepts a DnsPacket or packet-like object. | | server | The DnsServer instance. | | params | Route parameters collected by routers. | | source | Request source. UDP requests use 'udp'. | | type | First question qtype. | | class | First question qclass. |

env contains routing state:

| Property | Description | | --- | --- | | label | Reversed query labels. api.example.com becomes ['com', 'example', 'api']. | | labelPointer | Current label index used by label routers. | | i | Number of routing steps already performed. | | registery | Named handler registry. | | error | Error captured during error handling, when present. |

Response Behavior

ctx.respond() automatically copies these fields from the incoming request:

  • id
  • opcode
  • rd
  • questions

It also sets qr to DnsPacket.QR.RESPONSE.

Routers

Routers choose which handler runs for a request. A router can return:

  • A function handler.
  • An object with a .handle(ctx, env) method.
  • A string key that resolves through env.registery.
  • Another router object with a .route(env, ctx) method.
  • null or undefined, which falls back to the default handler.

FunctionRouter

Runs custom routing logic.

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.Function((env, ctx, ext) => {
    if (ctx.type === DnsPacket.TYPE.A) return 'a-record';
    return 'default';
}, { some: 'extra data' });

Constructor:

new FunctionRouter(fn, ext)

The route function receives (env, ctx, ext).

TypeRouter

Routes by DNS question type.

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.Type({
    [DnsPacket.TYPE.A]: 'a-record',
    [DnsPacket.TYPE.TXT]: 'txt-record',
    '*': 'default'
});

Constructor:

new TypeRouter(typeMap)

LabelRouter

Routes by query labels. The server stores labels in reverse DNS order, so api.example.com is matched as com, then example, then api.

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.Label('default', {
    '@ A .api.example.com': 'api-a-record',
    '@ .example.com': 'example-zone',
    '*': 'default'
});

Constructor:

new LabelRouter(end, map)

Arguments:

| Argument | Description | | --- | --- | | end | Handler returned when no more labels are available. | | map | Route map. |

Route key format:

[@][TYPE] .domain.name

Parts:

| Part | Description | | --- | --- | | @ | Match only when the full query name has been consumed. | | TYPE | Optional DNS type name from DnsPacket.TYPE, such as A or TXT. | | .domain.name | Domain name to match, prefixed with .. For example, node.j is written as .node.j. |

Examples:

| Key | Meaning | | --- | --- | | '@ A .node.j' | Match exactly node.j for A queries. | | '@ .node.j' | Match exactly node.j for any query type. | | 'A .node.j' | Match node.j and subdomains for A queries. | | '*' | Fallback route. |

Dynamic label parameters use %:name:

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.Label('default', {
    '@ A .%:host.example.com': (ctx) => {
        console.log(ctx.params.host);
        ctx.respond({ rcode: DnsPacket.RCODE.NO_ERROR_CONDITION });
    }
});

LabelArgRouter

Reads the current label into ctx.params and returns the next route.

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.LabelArg('host', 'next-handler');

Constructor:

new LabelArgRouter(paramName, next)

SetRegisteryRouter

Merges named handlers into env.registery and returns the next route.

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.SetRegistery({
    found: (ctx) => ctx.respond({ rcode: DnsPacket.RCODE.NO_ERROR_CONDITION })
}, 'found');

Constructor:

new SetRegisteryRouter(registery, next)

routerConstructors

Factory helpers for all router classes:

const {
    DnsPacket,
    routerConstructors: r
} = require('@jnode/dns');

const router = r.Type({
    [DnsPacket.TYPE.A]: 'a-record',
    '*': 'default'
});

Available factories:

r.Label(end, map)
r.Type(typeMap)
r.Function(fn, ext)
r.LabelArg(paramName, next)
r.SetRegistery(registery, next)

Handlers

Handlers produce DNS responses. A handler can be a plain function:

function handler(ctx, env) {
    ctx.respond({ rcode: DnsPacket.RCODE.NO_ERROR_CONDITION });
}

Or an object with a handle() method:

const handler = {
    handle(ctx, env) {
        ctx.respond({ rcode: DnsPacket.RCODE.NO_ERROR_CONDITION });
    }
};

ErrorHandler

Responds with a DNS response code.

const {
    DnsPacket,
    handlerConstructors: h
} = require('@jnode/dns');

const handler = h.Error(DnsPacket.RCODE.REFUSED);

Constructor:

new ErrorHandler(rcode)

Default rcode is DnsPacket.RCODE.SERVER_FAILURE.

PacketHandler

Responds with a specific packet.

const {
    DnsPacket,
    handlerConstructors: h
} = require('@jnode/dns');

const handler = h.Packet({
    rcode: DnsPacket.RCODE.REFUSED
});

Constructor:

new PacketHandler(packet)

packet may be a DnsPacket instance or a packet-like object.

RecordHandler

Creates a handler from raw record data.

const { handlerConstructors: h } = require('@jnode/dns');

const handler = h.Record(Buffer.from([127, 0, 0, 1]));

Constructor:

new RecordHandler(rdata)

If rdata is a string, it is converted with Buffer.from(rdata).

DnsClientHandler

Forwards the incoming request to an upstream DnsClient and responds with the upstream response.

const {
    DnsClient,
    handlerConstructors: h
} = require('@jnode/dns');

const client = new DnsClient(DnsClient.SERVERS.GOOGLE);
const handler = h.DnsClient(client);

Constructor:

new DnsClientHandler(client)

If no client is provided, a shared Cloudflare client is used.

Because forwarded responses are parsed and then serialized again, this handler is subject to the packet serializer limitations listed below.

handlerConstructors

Factory helpers for all handler classes:

const {
    DnsPacket,
    handlerConstructors: h
} = require('@jnode/dns');

const handler = h.Error(DnsPacket.RCODE.REFUSED);

Available factories:

h.Error(rcode)
h.Packet(packet)
h.Record(rdata)
h.DnsClient(client)

DNS Client API

new DnsClient(server, options)

Creates a DNS client.

const client = new DnsClient(DnsClient.SERVERS.CLOUDFLARE, {
    timeout: 2500,
    tries: 3,
    alwaysTcp: false,
    autoTcp: true,
    maxQueries: 128
});

server can be:

  • A DNS server IP string, such as '1.1.1.1'.
  • An array of DNS server IP strings.
  • Omitted, which uses Cloudflare.

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | timeout | number | 2500 | Query timeout in milliseconds. | | tries | number | 3 | Number of attempts per upstream server. | | alwaysTcp | boolean | false | Use TCP for every query. | | autoTcp | boolean | true | Retry with TCP when a UDP response is truncated. | | maxQueries | number | 128 | Maximum number of pending UDP queries. |

client.query(packet, options)

Sends a DNS query and resolves with a DnsPacket response.

const response = await client.query(packet);

Options:

| Option | Type | Default | Description | | --- | --- | --- | --- | | forceTcp | boolean | false | Use TCP for this query. |

The client tries each configured upstream server. For each server, it retries up to tries times before moving on to the next server. If all attempts fail, the last error is thrown.

Built-in DNS Servers

DnsClient.SERVERS.CLOUDFLARE
DnsClient.SERVERS.CLOUDFLARE_BLOCK_MALWARE
DnsClient.SERVERS.CLOUDFLARE_BLOCK_MALWARE_AND_ADULT_CONTENT
DnsClient.SERVERS.GOOGLE
DnsClient.SERVERS.QUAD9
DnsClient.SERVERS.QUAD9_SECURED
DnsClient.SERVERS.QUAD9_UNSECURED
DnsClient.SERVERS.ADGUARD
DnsClient.SERVERS.ADGUARD_NO_FILTERING
DnsClient.SERVERS.ADGUARD_FAMILY_PROTECTION

Each value is an array of resolver IP addresses.

Complete Examples

Custom Domain with Cloudflare Fallback

This server resolves node.j A queries to 127.0.0.1. Every other query falls through to the default handler and is forwarded to Cloudflare.

const {
    createServer,
    DnsPacket,
    DnsClient,
    routerConstructors: r,
    handlerConstructors: h
} = require('@jnode/dns');

const cloudflare = new DnsClient(DnsClient.SERVERS.CLOUDFLARE);

const registery = {
    node: (ctx) => {
        const question = ctx.packet.questions[0];

        ctx.respond({
            answers: [{
                aname: `${question.qname}.`,
                atype: DnsPacket.TYPE.A,
                aclass: question.qclass,
                ttl: 60,
                rdata: Buffer.from([127, 0, 0, 1])
            }]
        });
    },
    default: h.DnsClient(cloudflare),
    error: h.Error(DnsPacket.RCODE.SERVER_FAILURE)
};

const router = r.Label('default', {
    '@ A .node.j': 'node',
    '*': 'default'
});

const server = createServer(router, { registery });

server.on('error', console.error);
server.listen(() => {
    console.log('DNS server listening on UDP port 5353');
}, undefined, 5353);

Type-based Server

const {
    createServer,
    DnsPacket,
    routerConstructors: r,
    handlerConstructors: h
} = require('@jnode/dns');

const registery = {
    a: (ctx) => {
        ctx.respond({ rcode: DnsPacket.RCODE.REFUSED });
    },
    default: (ctx) => {
        ctx.respond({ rcode: DnsPacket.RCODE.NOT_IMPLEMENTED });
    },
    error: h.Error(DnsPacket.RCODE.SERVER_FAILURE)
};

const router = r.Type({
    [DnsPacket.TYPE.A]: 'a',
    '*': 'default'
});

const server = createServer(router, { registery });
server.listen(() => console.log('Listening on UDP port 5353'), undefined, 5353);

Label-based Server

const {
    createServer,
    DnsPacket,
    routerConstructors: r,
    handlerConstructors: h
} = require('@jnode/dns');

const router = r.Label('default', {
    '@ A .node.j': 'node',
    '*': 'default'
});

const server = createServer(router, {
    registery: {
        node: (ctx) => {
            const question = ctx.packet.questions[0];

            ctx.respond({
                answers: [{
                    aname: `${question.qname}.`,
                    atype: DnsPacket.TYPE.A,
                    aclass: question.qclass,
                    ttl: 60,
                    rdata: Buffer.from([127, 0, 0, 1])
                }]
            });
        },
        default: (ctx) => {
            ctx.respond({ rcode: DnsPacket.RCODE.NAME_ERROR });
        },
        error: h.Error(DnsPacket.RCODE.SERVER_FAILURE)
    }
});

server.listen(() => console.log('Listening on UDP port 5353'), undefined, 5353);

Implementation Notes and Limitations

  • UDP server requests are handled. The server creates a TCP listener, but TCP DNS request handling is not currently implemented.
  • server.close() currently closes the UDP socket.
  • DNS rdata is treated as raw bytes. Type-specific record helpers are not included.
  • DnsPacket.toBuffer() serializes questions and answers. Authority and additional record serialization is not currently implemented.
  • DnsPacket.toBuffer() currently has a name-compression serialization bug when an answer name reuses a previously serialized label suffix, such as the same name used in the question.
  • The code uses the public spelling registery; use that spelling in options and routers.
  • DNS name compression is parsed. Serialization is intended for straightforward packets and may not compress names.

Development

Clone the repository and run:

npm install
npm test

The current test script prints a package smoke-test message.

License

MIT License. See LICENSE.