#!/usr/bin/env node 'use strict'; var cli = require('cli'); var pkg = require('../package.json'); var dig = require('dig.js/dns-request'); var dgram = require('dgram'); var dnsjs = require('dns-suite'); var crypto = require('crypto'); var common = require('dig.js/common'); var defaultNameservers = require('dns').getServers(); var hexdump; cli.parse({ // 'b': [ false, 'set source IP address (defaults to 0.0.0.0)', 'string' ] 'class': [ 'c', 'class (defaults to IN)', 'string', 'IN' ] , 'debug': [ false, 'more verbose output', 'boolean', false ] //, 'insecure': [ false, 'turn off RaNDOm cAPS required for securing queries'] //, 'ipv4': [ '4', 'use ipv4 exclusively (defaults to false)', 'boolean', false ] //, 'ipv6': [ '6', 'use ipv6 exclusively (defaults to false)', 'boolean', false ] //, 'json': [ false, 'output results as json', 'string' ] //, 'lint': [ false, 'attack (in the metaphorical sense) a nameserver with all sorts of queries to test for correct responses', 'string', false ] , 'mdns': [ false, "Alias for setting defaults to -p 5353 @224.0.0.251 -t PTR -q _services._dns-sd._udp.local and waiting for multiple responses", 'boolean', false ] , 'timeout': [ false, "How long, in milliseconds, to wait for a response. Alias of +time=", 'int', false ] , 'output': [ 'o', 'output prefix to use for writing query and response(s) to disk', 'file' ] , 'input': [ false, 'input file to use for authoritative responses', 'file' ] , 'address': [ false, 'ip address(es) to listen on (defaults to 0.0.0.0,::0)', 'string' ] , 'port': [ 'p', 'port (defaults to 53 for dns and 5353 for mdns)', 'int' ] , 'nameserver': [ false, 'the nameserver(s) to use for recursive lookups (defaults to ' + defaultNameservers.join(',') + ')', 'string' ] //, 'serve': [ 's', 'path to json file with array of responses to issue for given queries', 'string' ] //, 'type': [ 't', 'type (defaults to ANY for dns and PTR for mdns)', 'string' ] //, 'query': [ 'q', 'a superfluous explicit option to set the query as a command line flag' ] }); cli.main(function (args, cli) { args.forEach(function (arg) { if (arg === '+norecurse') { if (cli.norecurse) { console.error("'+norecurse' was specified more than once"); process.exit(1); return; } cli.norecurse = true; return; } }); if (cli.mdns) { if (!cli.type) { cli.type = cli.t = 'PTR'; } if (!cli.port) { cli.port = cli.p = 5353; } if (!cli.nameserver) { cli.nameserver = '224.0.0.251'; } if (!cli.query) { cli.query = '_services._dns-sd._udp.local'; } if (!('timeout' in cli)) { cli.timeout = 3000; } } else { if (!cli.port) { cli.port = cli.p = 53; } } var handlers = {}; var server = dgram.createSocket({ type: cli.udp6 ? 'udp6' : 'udp4' , reuseAddr: true }); server.bind({ port: cli.port , address: cli.address }); handlers.onError = function (err) { if ('EACCES' === err.code) { console.error(""); console.error("EACCES: Couldn't bind to port. You probably need to use sudo, authbind, or setcap."); console.error(""); process.exit(123); return; } console.error("error:", err.stack); server.close(); }; handlers.onMessage = function (nb, rinfo) { console.log('[DEBUG] got a message'); var queryAb = nb.buffer.slice(nb.byteOffset, nb.byteOffset + nb.byteLength); var query; var count; try { query = dnsjs.DNSPacket.parse(queryAb); } catch(e) { // TODO log bad queries (?) console.error("Could not parse DNS query, ignoring."); try { hexdump = require('hexdump.js').hexdump; console.error(hexdump(queryAb)); console.error(''); } catch(e) { // ignore } return; } if (cli.debug) { console.log(''); console.log('DNS Question:'); console.log(''); console.log(query); console.log(''); try { hexdump = require('hexdump.js').hexdump; console.log(hexdump(queryAb)); console.log(''); } catch(e) { // ignore } } dig.logQuestion(query); /* console.log(';; Got question:'); console.log(';; ->>HEADER<<-'); console.log(JSON.stringify(query.header)); console.log(''); console.log(';; QUESTION SECTION:'); query.question.forEach(function (q) { console.log(';' + q.name + '.', ' ', q.className, q.typeName); }); */ function print(q) { var printer = common.printers[q.typeName] || common.printers.ANY; printer(q); } if (query.answer.length) { console.error('[ERROR] Query contains an answer section:'); console.log(';; ANSWER SECTION:'); query.answer.forEach(print); } if (query.authority.length) { console.log(''); console.error('[ERROR] Query contains an authority section:'); console.log(';; AUTHORITY SECTION:'); query.authority.forEach(print); } if (query.additional.length) { console.log(''); console.error('[ERROR] Query contains an additional section:'); console.log(';; ADDITIONAL SECTION:'); query.additional.forEach(print); } console.log(''); console.log(';; MSG SIZE rcvd: ' + nb.byteLength); console.log(''); if (cli.output) { console.log(''); common.writeQuery(cli, query, queryAb); //common.writeResponse(opts, query, nb, packet); } function sendEmptyResponse(query, nx) { // rcode // 0 SUCCESS // manages this domain and found a record // 3 NXDOMAIN // manages this domain, but doesn't have a record // 5 REFUSED // doesn't manage this domain var newAb; var emptyResp = { header: { id: query.header.id // require('crypto').randomBytes(2).readUInt16BE(0) , qr: 1 , opcode: 0 , aa: 0 // TODO it may be authoritative , tc: 0 , rd: query.header.rd , ra: cli.norecurse ? 0 : 1 // TODO is this bit dependent on the rd bit? , rcode: nx ? 3 : 0 // no error } , question: [] , answer: [] , authority: [] , additional: [] }; query.question.forEach(function (q) { emptyResp.question.push({ name: q.name , type: q.type , typeName: q.typeName , class: q.class , className: q.className }); }); try { newAb = dnsjs.DNSPacket.write(emptyResp); } catch(e) { console.error("Could not write DNS response"); console.error(emptyResp); return; } server.send(newAb, rinfo.port, rinfo.address, function () { console.log('[DEV] response sent (empty)'); }); } function sendResponse(newPacket) { var newAb; try { newAb = dnsjs.DNSPacket.write(newPacket); } catch(e) { console.error("Could not write DNS response"); console.error(newPacket); return; } server.send(newAb, rinfo.port, rinfo.address, function () { console.log('[DEV] response sent (local query)'); }); } function recurse() { if (!query.header.rd) { console.log("[Could not answer. Sent empty response.]"); sendEmptyResponse(query, true); return; } if (cli.norecurse) { console.log("[Could not answer. Sent empty response.]"); sendEmptyResponse(query, true); return; } // TODO newQuery var newResponse = { header: { id: query.header.id // require('crypto').randomBytes(2).readUInt16BE(0) , qr: 0 , opcode: 0 , aa: query.header.aa ? 1 : 0 // NA? not sure what this would do , tc: 0 // NA , rd: 1 , ra: 0 // NA , rcode: 0 // NA } , question: [] , answer: [] , authority: [] , additional: [] }; query.question.forEach(function (q) { newResponse.question.push({ name: q.name , type: q.type , typeName: q.typeName , class: q.class , className: q.className }); function updateCount() { var newAb; count -= 1; if (!count) { try { newAb = dnsjs.DNSPacket.write(newResponse); } catch(e) { console.error("Could not write DNS response"); console.error(newResponse); return; } server.send(newAb, rinfo.port, rinfo.address, function () { console.log('[DEV] response sent'); }); } } var opts = { onError: function () { updateCount(); } , onMessage: function (packet) { (packet.answer||[]).forEach(function (a) { // TODO copy each relevant property console.log('ans', JSON.stringify(a, null, 2)); newResponse.answer.push(a); }); (packet.authority||[]).forEach(function (a) { // TODO copy each relevant property console.log('auth', JSON.stringify(a, null, 2)); newResponse.authority.push(a); }); (packet.additional||[]).forEach(function (a) { // TODO copy each relevant property console.log('add', JSON.stringify(a, null, 2)); newResponse.additional.push(a); }); updateCount(); } , onListening: function () {} , onSent: function (/*res*/) { /* if (cli.debug) { console.log(''); console.log('request sent to', res.nameserver); } */ console.log('[DEV] response sent (recurse)'); } , onTimeout: function (res) { console.log(";; [" + q.name + "] connection timed out; no servers could be reached"); console.log(";; [timed out after " + res.timeout + "ms and 1 tries]"); } , onClose: function () { console.log(''); } , mdns: cli.mdns , nameserver: cli.nameserver , port: cli.port , timeout: cli.timeout }; //dig.resolve(queryAb, opts); dig.resolveJson(query, opts); console.log(';' + q.name + '.', ' ', q.className, q.typeName); }); } count = query.question.length; if (!count) { sendEmptyResponse(query); return; } // TODO get local answer first, if available require('../lib/dns-store').query(cli.input, query, function (err, resp) { if (err) { recurse(); return; } sendResponse(resp); }); }; handlers.onListening = function () { /*jshint validthis:true*/ var server = this; var nameserver = cli.nameserver; var index; if (!nameserver) { index = crypto.randomBytes(2).readUInt16BE(0) % defaultNameservers.length; nameserver = defaultNameservers[index]; if (cli.debug) { console.log(index, defaultNameservers); } } if (cli.mdns || '224.0.0.251' === cli.nameserver) { server.setBroadcast(true); server.addMembership(cli.nameserver); } console.log(''); console.log('Bound and Listening:'); console.log(server.address().address + '#' + server.address().port); }; console.log(''); if (!cli.nocmd) { console.log('; <<>> digd.js v' + pkg.version + ' <<>> ' + process.argv.slice(2).join(' ')); console.log(';; global options: +cmd'); } server.on('error', handlers.onError); server.on('message', handlers.onMessage); server.on('listening', handlers.onListening); });