UNPKG

dig.js

Version:

Create and capture DNS and mDNS query and response packets to disk as binary and/or JSON. Options are similar to the Unix `dig` command.

350 lines (314 loc) 11.4 kB
#!/usr/bin/env node 'use strict'; var dig = require('../dns-request'); var cli = require('cli'); var defaultNameservers = require('dns').getServers(); var typeRe = /^type\d+$/i; 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' ] , 'port': [ 'p', 'port (defaults to 53 for dns and 5353 for mdns)', 'int' ] , 'nameserver': [ false, 'the nameserver to use for DNS resolution (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', 'string' ] , 'norecase': [ false, 'Disable dns0x20 security checking (mixed casing). See https://dyn.com/blog/use-of-bit-0x20-in-dns-labels/' ] , 'recase': [ false, "Print the dns0x20 casing as-is rather than converting it back to lowercase. This is the default when explicitly using mixed case." ] }); var common = require('../common.js'); cli.main(function (args, cli) { cli.implicitType = cli.type; cli.implicitQuery = cli.query; args.forEach(function (arg) { if (typeRe.test(arg) || -1 !== common.types.concat([ 'ANY' ]).indexOf(arg.toUpperCase())) { if (cli.implicitType) { console.error("'type' was specified more than once"); process.exit(1); return; } cli.implicitType = cli.t = arg.toUpperCase(); return; } if (arg === '+aaonly' || arg === '+aaflag') { if (cli.aaonly) { console.error("'+aaonly' was specified more than once"); process.exit(1); return; } cli.aaonly = true; return; } if (arg === '+norecurse') { if (cli.norecurse) { console.error("'+norecurse' was specified more than once"); process.exit(1); return; } cli.norecurse = true; return; } if (/^\+time=/.test(arg)) { if (cli.timeout) { console.error("'+time=' was specified more than once"); process.exit(1); return; } cli.timeout = Math.round(parseInt(arg.replace(/\+time=/, ''), 10) * 1000); return; } if (/^@/.test(arg)) { if (cli.nameserver) { console.error("'@server' was specified more than once"); process.exit(1); return; } cli.nameserver = cli.n = arg.substr(1); return; } if ('string' === typeof cli.implicitQuery) { console.error("'query' was specified more than once or unrecognized flag: " + cli.implicitQuery + ", " + arg); process.exit(1); return; } cli.implicitQuery = cli.q = arg; }); // it can happen that a TLD is created with the name of a common type if (!cli.type && cli.implicitType && !cli.implicitQuery) { cli.implicitQuery = cli.implicitType; cli.implicitType = null; } if ('string' === typeof cli.implicitQuery) { cli.query = cli.implicitQuery; } if (cli.implicitType) { cli.type = cli.implicitType; } if ('string' !== typeof cli.query) { console.error(''); console.error('Usage:'); console.error('dig.js [@server] [TYPE] [domain]'); console.error(''); console.error('Example:'); console.error('dig.js daplie.com'); console.error(''); process.exit(1); } if (cli.query !== cli.query.toLowerCase()) { cli.norecase = true; } 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 ('string' !== typeof cli.query) { cli.query = '_services._dns-sd._udp.local'; } if (!cli.timeout) { cli.timeout = 3000; } } else { if (!cli.timeout) { cli.timeout = 5000; } } if (!cli.norecase) { cli.casedQuery = cli.query.split('').map(function (ch) { // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is // ch = ch | 0x20; return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); }).join(''); } else { cli.casedQuery = cli.query; } if (!cli.type) { cli.type = cli.t = 'ANY'; } if (typeRe.test(cli.type)) { cli.rawType = parseInt(cli.type.replace('type', ''), 10); } if (!cli.port) { cli.port = cli.p = 53; } if (!cli.class) { cli.class = cli.c = 'IN'; } var query = { header: { id: require('crypto').randomBytes(2).readUInt16BE(0) , qr: 0 , opcode: 0 , aa: cli.aaonly ? 1 : 0 // NA , tc: 0 // NA , rd: cli.norecurse ? 0 : 1 , ra: 0 // NA , rcode: 0 // NA } , question: [ { name: cli.casedQuery , type: cli.rawType , typeName: cli.rawType ? undefined : cli.type , className: cli.class } ] }; var dnsjs = require('dns-suite'); var queryAb = dnsjs.DNSPacket.write(query); var hexdump = require('hexdump.js').hexdump; if (cli.debug) { console.log(''); console.log('DNS Question:'); console.log(''); console.log(query); console.log(''); console.log(hexdump(queryAb)); console.log(''); console.log(dnsjs.DNSPacket.parse(queryAb)); console.log(''); } cli.onError = function (err) { console.error("error:", err.stack); }; cli.onMessage = function (nb) { var packet = dnsjs.DNSPacket.parse(nb.buffer.slice(nb.byteOffset, nb.byteOffset + nb.byteLength)); var fail0x20; if (packet.id !== query.id) { console.error('[SECURITY] ignoring packet for \'' + packet.question[0].name + '\' due to mismatched id'); console.error(packet); return; } if (cli.debug) { console.log(''); console.log('DNS Response:'); console.log(packet); } packet.question.forEach(function (q) { // if (-1 === q.name.lastIndexOf(cli.casedQuery)) if (q.name !== cli.casedQuery) { fail0x20 = q.name; } }); if (!cli.norecase && !cli.recase) { [ 'question', 'answer', 'authority', 'additional' ].forEach(function (group) { (packet[group]||[]).forEach(function (a) { var an = a.name; var i = cli.query.toLowerCase().lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM var j = a.name.toLowerCase().lastIndexOf(cli.query.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM // it's important to note that these should only relpace changes in casing that we expected // any abnormalities should be left intact to go "huh?" about // TODO detect abnormalities? if (-1 !== i) { // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) a.name = a.name.replace(cli.casedQuery.substr(i), cli.query.substr(i)); } else if (-1 !== j) { // "www.example.com".replace("EXamPLE.cOm", "example.com") a.name = a.name.substr(0, j) + a.name.substr(j).replace(cli.casedQuery, cli.query); } // NOTE: right now this assumes that anything matching the query matches all the way to the end // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly // (but I don't think it should need to) if (a.name.length !== an.length) { console.error("[ERROR] question / answer mismatch: '" + an + "' != '" + a.length + "'"); console.error(a); } }); }); } if (fail0x20) { console.warn(""); console.warn(";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" + cli.casedQuery + "' but got response for '" + fail0x20 + "'."); console.warn(""); } console.log(';; Got answer:'); dig.logQuestion(packet); function print(q) { var printer = common.printers[q.typeName] || common.printers.ANY; printer(q); } if (packet.answer.length) { console.log(''); console.log(';; ANSWER SECTION:'); packet.answer.forEach(print); } if (packet.authority.length) { console.log(''); console.log(';; AUTHORITY SECTION:'); packet.authority.forEach(print); } if (packet.additional.length) { console.log(''); console.log(';; ADDITIONAL SECTION:'); packet.additional.forEach(print); } console.log(''); console.log(';; Query time: ' + (Date.now() - cli._ts) + ' msec'); // ;; SERVER: 8.8.8.8#53(8.8.8.8) console.log(';; SERVER: ' + cli._nameserver + '#' + cli.port + '(' + cli._nameserver + ')'); // TODO ;; WHEN: Fri Sep 15 18:25:53 2017 console.log(';; WHEN: ' + new Date().toString()); console.log(';; MSG SIZE rcvd: ' + nb.byteLength); console.log(''); if (cli.output) { console.log(''); common.writeQuery(cli, query, queryAb); common.writeResponse(cli, query, nb, packet); } }; cli.onListening = function () { /*jshint validthis:true*/ var server = this; if (cli.debug) { console.log(''); console.log('Bound and Listening:', server.type); console.log(server.address()); } // technicially this should be a seperate event if (cli.debug) { console.log("querying '" + server.nameserver + "':'" + cli.port + "'"); } }; console.log(''); if (!cli.nocmd) { console.log('; <<>> dig.js ' + 'v0.0.0' + ' <<>> ' + process.argv.slice(2).join(' ').replace(cli.query, cli.casedQuery)); console.log(';; global options: +cmd'); } var opts = { onError: cli.onError , onMessage: cli.onMessage , onListening: cli.onListening , onSent: function (res) { cli._nameserver = res.nameserver; cli._ts = Date.now(); if (cli.debug) { console.log(''); console.log('request sent to', res.nameserver); } } , onTimeout: function (res) { console.log(";; 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); });