@cepharum/dnsd

0.10.2 • Public • Published

dnsd: DNS encoder, decoder, and server

dnsd is a Node.js package for working with DNS. It converts binary DNS messages to and from convenient JavaScript objects; and it provides a server API, for running a custom name server.

This module is a fork of dnsd originally developed for Iris Couch. We've basically improved the code and started adding support for EDNS. Currently, we are using it for dynamic exposure of services in a Docker swarm.

dnsd is available as an npm module.

$ npm install dnsd

Example: Running a server

This simple DNS server responds with an "A" (address) record of 1.2.3.4 for every request.

const dnsd = require( "dnsd" );

dnsd.createServer( ( req, res ) => {
    res.end( "1.2.3.4" );
}, {
    // provide custom defaults here
    ttl: 3600,
} )
    .zone( "example" )
    .listen( 5353, "127.0.0.1" )
    .once( "listening", () => {
        console.log( "Server is running at 127.0.0.1:5353" );
    } );

Now test your server:

$ dig @localhost -p 5353 foo.example A

; <<>> DiG 9.8.1-P1 <<>> @localhost -p 5353 foo.example A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27955
;; flags: qr rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;foo.example.			IN	A

;; ANSWER SECTION:
foo.example.		3600	IN	A	1.2.3.4

;; Query time: 1 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1)
;; WHEN: Wed Aug  8 05:10:40 2012
;; MSG SIZE  rcvd: 45

This example logs all requests. For address (A) queries, it returns two records, with a random TTL, and the final octet of the IP address is the length of the hostname queried.

const dnsd = require( "dnsd" );

dnsd.createServer(handler)
    .zone("example.com", "ns1.example.com", "us@example.com", "now", "2h", "30m", "2w", "10m" )
    .listen( 5353, "127.0.0.1" );

function handler(req, res) {
    const { remoteAddress, remotePort, type } = req.connection;

    console.log( "%s:%s/%s %j", remoteAddress, remotePort, type, req );
    
    const question = res.question[0];
    const hostname = question.name;
    const length = hostname.length;
    const ttl = Math.floor( Math.random() * 3600 );

    if ( question.type === "A" ) {
        res.answer.push( { name:hostname, type:"A", data:"1.1.1." + length, ttl } );
        res.answer.push( { name:hostname, type:"A", data:"2.2.2." + length, ttl } );
    }

    res.end()
}

Test the SOA response:

$ dig @localhost -p 5353 example.com soa

; <<>> DiG 9.8.1-P1 <<>> @localhost -p 5353 example.com soa
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30176
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;example.com.			IN	SOA

;; ANSWER SECTION:
example.com.		600	IN	SOA	ns1.example.com. us.example.com. 1344403648 7200 1800 1209600 600

;; Query time: 5 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1)
;; WHEN: Wed Aug  8 05:27:32 2012
;; MSG SIZE  rcvd: 72

And test the address (A) response:

$ dig @localhost -p 5353 example.com a

; <<>> DiG 9.8.1-P1 <<>> @localhost -p 5353 example.com a
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19419
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;example.com.			IN	A

;; ANSWER SECTION:
example.com.		1222	IN	A	1.1.1.11
example.com.		1222	IN	A	2.2.2.11

;; Query time: 1 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1)
;; WHEN: Wed Aug  8 05:27:34 2012
;; MSG SIZE  rcvd: 61

Server output for these queries:

Server running at 127.0.0.1:5353
127.0.0.1:34427/udp4 {"id":30176,"type":"request","responseCode":0,"opcode":"query","authoritative":false,"truncated":false,"recursionDesired":true,"recursionAvailable":false,"authenticated":false,"checkingDisabled":false,"question":[{"name":"example.com","type":"SOA","class":"IN"}]}
127.0.0.1:59596/udp4 {"id":19419,"type":"request","responseCode":0,"opcode":"query","authoritative":false,"truncated":false,"recursionDesired":true,"recursionAvailable":false,"authenticated":false,"checkingDisabled":false,"question":[{"name":"example.com","type":"A","class":"IN"}]}

Example: MX Records

This is an example if you need to route your mail server with an MX record.

// Example MX response with dnsd
//
// To test:
// 1. Run this program
// 2. dig @localhost -p 5353 example.com mx
 
const dnsd = require( "dnsd" );
 
dnsd.createServer( handler )
    .zone( "example.com", "ns1.example.com", "us@example.com", "now", "2h", "30m", "2w", "10m" )
    .listen( 5353, "127.0.0.1" );
 
function handler(req, res) {
    const question = res.question && res.question[0]
    
    if ( question.type === "MX" ) {
        console.log( "MX lookup for domain: %s", question.name );
        
        res.answer.push( { name:question.name, type:"MX", data:{ weight: 10, name: "mail.example.com" } } );
        res.answer.push( { name:question.name, type:"MX", data:{ weight: 20, name: "mail.backupexample.com" } } );
    }
    
    return res.end();
}

The MX data attribute needs to be an Array to work properly, the first value is the priority, the second is the server. This server name must be a domain string and not an IP address. Make sure you have an A record or CNAME setup for this.

See http://support.google.com/a/bin/answer.py?hl=en&answer=140034 for more info on MX records and configuration.

Example: Parse a message

const fs = require("fs")
const dnsd = require("dnsd")

const msg_file = require.resolve( "dnsd/_test_data/registry.npmjs.org-response" );
const msg_data = fs.readFileSync( msg_file );
const message = dnsd.decode( msg_data );

console.dir( message );

Output

{ 
    id: 34233,
    type: "response",
    responseCode: 0,
    opcode: "query",
    authoritative: false,
    truncated: false,
    recursionDesired: true,
    recursionAvailable: true,
    authenticated: false,
    checkingDisabled: false,
    question: [ { name: "registry.npmjs.org", type: "A", class: "IN" } ],
    answer: [ 
        { 
            name: "registry.npmjs.org",
            type: "CNAME",
            class: "IN",
            ttl: 85,
            data: "isaacs.iriscouch.net",
        }, { 
            name: "isaacs.iriscouch.net",
            type: "CNAME",
            class: "IN",
            ttl: 2821,
            data: "ec2-23-23-147-24.compute-1.amazonaws.com",
         }, { 
            name: "ec2-23-23-147-24.compute-1.amazonaws.com",
            type: "A",
            class: "IN",
            ttl: 356336,
            data: "23.23.147.24",
         }, 
    ],
}

Example: Encode a message

const dnsd = require("dnsd")

const questions = [ { name: "example.com", class: "IN", type: "TXT" } ];
const message = { 
    type: "query", 
    id: 123, 
    opcode: "query", 
    recursionDesired: true, 
    question: questions
};
const msg_data = dnsd.encode( message );

console.log( "Encoded = %j", Array.prototype.slice.apply( msg_data ) );

message = dnsd.decode( msg_data );

console.log( "Round trip:" );
console.dir( message );

Output:

Encoded = [0,123,1,0,0,1,0,0,0,0,0,0,7,101,120,97,109,112,108,101,3,99,111,109,0,0,16,0,1]
Round trip:
{ id: 123,
  type: 'request',
  responseCode: 0,
  opcode: 'query',
  authoritative: false,
  truncated: false,
  recursionDesired: true,
  recursionAvailable: false,
  authenticated: false,
  checkingDisabled: false,
  question: [ { name: 'example.com', type: 'TXT', class: 'IN' } ] }

Convenience Support

  • You can pass a value to res.end(), with special handling depending on type:
    • Array: those values will be added to the res.answer section.
    • Object: that object will be sent as a response (res is unused).
    • String: the response will add an anser A record with your value as the IP address.
  • Automatically respond to SOA queries with the SOA record.
  • Responses to an A query with no answers will add the SOA record to the response.
  • If the response records are missing a TTL, use the one from the .zone() definition (the SOA record)

Tests

This code is tested with node-tap.

$ tap test
ok test/api.js ........................................ 10/10
ok test/convenience.js ................................ 22/22
ok test/message.js .................................. 176/176
ok test/print.js ...................................... 10/10
ok test/server.js ..................................... 35/35
total ............................................... 253/253

ok

License

Apache 2.0

See the Apache 2.0 license.

Dependents (1)

Package Sidebar

Install

npm i @cepharum/dnsd

Weekly Downloads

1

Version

0.10.2

License

none

Unpacked Size

440 kB

Total Files

27

Last publish

Collaborators

  • simon.friedo
  • soletan