Skip to content

Commit

Permalink
test: add non-internet resolveAny tests
Browse files Browse the repository at this point in the history
This is a bit of a check to see how people feel about having this kind
of test.

Ref: #13137
PR-URL: #13883
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
  • Loading branch information
addaleax committed Jul 15, 2017
1 parent 185b5d5 commit 9c1e270
Show file tree
Hide file tree
Showing 3 changed files with 378 additions and 0 deletions.
290 changes: 290 additions & 0 deletions test/common/dns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
/* eslint-disable required-modules */
'use strict';

// Naïve DNS parser/serializer.

const assert = require('assert');
const os = require('os');

const types = {
A: 1,
AAAA: 28,
NS: 2,
CNAME: 5,
SOA: 6,
PTR: 12,
MX: 15,
TXT: 16,
ANY: 255
};

const classes = {
IN: 1
};

function readDomainFromPacket(buffer, offset) {
assert.ok(offset < buffer.length);
const length = buffer[offset];
if (length === 0) {
return { nread: 1, domain: '' };
} else if ((length & 0xC0) === 0) {
offset += 1;
const chunk = buffer.toString('ascii', offset, offset + length);
// Read the rest of the domain.
const { nread, domain } = readDomainFromPacket(buffer, offset + length);
return {
nread: 1 + length + nread,
domain: domain ? `${chunk}.${domain}` : chunk
};
} else {
// Pointer to another part of the packet.
assert.strictEqual(length & 0xC0, 0xC0);
// eslint-disable-next-line
const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
return {
nread: 2,
domain: readDomainFromPacket(buffer, pointeeOffset)
};
}
}

function parseDNSPacket(buffer) {
assert.ok(buffer.length > 12);

const parsed = {
id: buffer.readUInt16BE(0),
flags: buffer.readUInt16BE(2),
};

const counts = [
['questions', buffer.readUInt16BE(4)],
['answers', buffer.readUInt16BE(6)],
['authorityAnswers', buffer.readUInt16BE(8)],
['additionalRecords', buffer.readUInt16BE(10)]
];

let offset = 12;
for (const [ sectionName, count ] of counts) {
parsed[sectionName] = [];
for (let i = 0; i < count; ++i) {
const { nread, domain } = readDomainFromPacket(buffer, offset);
offset += nread;

const type = buffer.readUInt16BE(offset);

const rr = {
domain,
cls: buffer.readUInt16BE(offset + 2),
};
offset += 4;

for (const name in types) {
if (types[name] === type)
rr.type = name;
}

if (sectionName !== 'questions') {
rr.ttl = buffer.readInt32BE(offset);
const dataLength = buffer.readUInt16BE(offset);
offset += 6;

switch (type) {
case types.A:
assert.strictEqual(dataLength, 4);
rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
`${buffer[offset + 2]}.${buffer[offset + 3]}`;
break;
case types.AAAA:
assert.strictEqual(dataLength, 16);
rr.address = buffer.toString('hex', offset, offset + 16)
.replace(/(.{4}(?!$))/g, '$1:');
break;
case types.TXT:
{
let position = offset;
rr.entries = [];
while (position < offset + dataLength) {
const txtLength = buffer[offset];
rr.entries.push(buffer.toString('utf8',
position + 1,
position + 1 + txtLength));
position += 1 + txtLength;
}
assert.strictEqual(position, offset + dataLength);
break;
}
case types.MX:
{
rr.priority = buffer.readInt16BE(buffer, offset);
offset += 2;
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.exchange = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.NS:
case types.CNAME:
case types.PTR:
{
const { nread, domain } = readDomainFromPacket(buffer, offset);
rr.value = domain;
assert.strictEqual(nread, dataLength);
break;
}
case types.SOA:
{
const mname = readDomainFromPacket(buffer, offset);
const rname = readDomainFromPacket(buffer, offset + mname.nread);
rr.nsname = mname.domain;
rr.hostmaster = rname.domain;
const trailerOffset = offset + mname.nread + rname.nread;
rr.serial = buffer.readUInt32BE(trailerOffset);
rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
rr.retry = buffer.readUInt32BE(trailerOffset + 8);
rr.expire = buffer.readUInt32BE(trailerOffset + 12);
rr.minttl = buffer.readUInt32BE(trailerOffset + 16);

assert.strictEqual(trailerOffset + 20, dataLength);
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
offset += dataLength;
}

parsed[sectionName].push(rr);

assert.ok(offset <= buffer.length);
}
}

assert.strictEqual(offset, buffer.length);
return parsed;
}

function writeIPv6(ip) {
const parts = ip.replace(/^:|:$/g, '').split(':');
const buf = Buffer.alloc(16);

let offset = 0;
for (const part of parts) {
if (part === '') {
offset += 16 - 2 * (parts.length - 1);
} else {
buf.writeUInt16BE(parseInt(part, 16), offset);
offset += 2;
}
}

return buf;
}

function writeDomainName(domain) {
return Buffer.concat(domain.split('.').map((label) => {
assert(label.length < 64);
return Buffer.concat([
Buffer.from([label.length]),
Buffer.from(label, 'ascii')
]);
}).concat([Buffer.alloc(1)]));
}

function writeDNSPacket(parsed) {
const buffers = [];
const kStandardResponseFlags = 0x8180;

buffers.push(new Uint16Array([
parsed.id,
parsed.flags === undefined ? kStandardResponseFlags : parsed.flags,
parsed.questions && parsed.questions.length,
parsed.answers && parsed.answers.length,
parsed.authorityAnswers && parsed.authorityAnswers.length,
parsed.additionalRecords && parsed.additionalRecords.length,
]));

for (const q of parsed.questions) {
assert(types[q.type]);
buffers.push(writeDomainName(q.domain));
buffers.push(new Uint16Array([
types[q.type],
q.cls === undefined ? classes.IN : q.cls
]));
}

for (const rr of [].concat(parsed.answers,
parsed.authorityAnswers,
parsed.additionalRecords)) {
if (!rr) continue;

assert(types[rr.type]);
buffers.push(writeDomainName(rr.domain));
buffers.push(new Uint16Array([
types[rr.type],
rr.cls === undefined ? classes.IN : rr.cls
]));
buffers.push(new Int32Array([rr.ttl]));

const rdLengthBuf = new Uint16Array(1);
buffers.push(rdLengthBuf);

switch (rr.type) {
case 'A':
rdLengthBuf[0] = 4;
buffers.push(new Uint8Array(rr.address.split('.')));
break;
case 'AAAA':
rdLengthBuf[0] = 16;
buffers.push(writeIPv6(rr.address));
break;
case 'TXT':
const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
// Total length of all strings + 1 byte each for their lengths.
rdLengthBuf[0] = rr.entries.length + total;
for (const txt of rr.entries) {
buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
buffers.push(Buffer.from(txt));
}
break;
case 'MX':
rdLengthBuf[0] = 2;
buffers.push(new Uint16Array([rr.priority]));
// fall through
case 'NS':
case 'CNAME':
case 'PTR':
{
const domain = writeDomainName(rr.exchange || rr.value);
rdLengthBuf[0] += domain.length;
buffers.push(domain);
break;
}
case 'SOA':
{
const mname = writeDomainName(rr.nsname);
const rname = writeDomainName(rr.hostmaster);
rdLengthBuf[0] = mname.length + rname.length + 20;
buffers.push(mname, rname);
buffers.push(new Uint32Array([
rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl
]));
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}
}

return Buffer.concat(buffers.map((typedArray) => {
const buf = Buffer.from(typedArray.buffer,
typedArray.byteOffset,
typedArray.byteLength);
if (os.endianness() === 'LE') {
if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
}
return buf;
}));
}

module.exports = { types, classes, writeDNSPacket, parseDNSPacket };
35 changes: 35 additions & 0 deletions test/parallel/test-dns-resolveany-bad-ancount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
const dnstools = require('../common/dns');
const dns = require('dns');
const assert = require('assert');
const dgram = require('dgram');

const server = dgram.createSocket('udp4');

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;
assert.strictEqual(domain, 'example.org');

const buf = dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain },
});
// Overwrite the # of answers with 2, which is incorrect.
buf.writeUInt16LE(2, 6);
server.send(buf, port, address);
}));

server.bind(0, common.mustCall(() => {
const address = server.address();
dns.setServers([`127.0.0.1:${address.port}`]);

dns.resolveAny('example.org', common.mustCall((err) => {
assert.strictEqual(err.code, 'EBADRESP');
assert.strictEqual(err.syscall, 'queryAny');
assert.strictEqual(err.hostname, 'example.org');
server.close();
}));
}));
53 changes: 53 additions & 0 deletions test/parallel/test-dns-resolveany.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';
const common = require('../common');
const dnstools = require('../common/dns');
const dns = require('dns');
const assert = require('assert');
const dgram = require('dgram');

const answers = [
{ type: 'A', address: '1.2.3.4', ttl: 123 },
{ type: 'AAAA', address: '::42', ttl: 123 },
{ type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 },
{ type: 'NS', value: 'foobar.org', ttl: 457 },
{ type: 'TXT', entries: [ 'v=spf1 ~all', 'xyz' ] },
{ type: 'PTR', value: 'baz.org', ttl: 987 },
{
type: 'SOA',
nsname: 'ns1.example.com',
hostmaster: 'admin.example.com',
serial: 156696742,
refresh: 900,
retry: 900,
expire: 1800,
minttl: 60
},
];

const server = dgram.createSocket('udp4');

server.on('message', common.mustCall((msg, { address, port }) => {
const parsed = dnstools.parseDNSPacket(msg);
const domain = parsed.questions[0].domain;
assert.strictEqual(domain, 'example.org');

server.send(dnstools.writeDNSPacket({
id: parsed.id,
questions: parsed.questions,
answers: answers.map((answer) => Object.assign({ domain }, answer)),
}), port, address);
}));

server.bind(0, common.mustCall(() => {
const address = server.address();
dns.setServers([`127.0.0.1:${address.port}`]);

dns.resolveAny('example.org', common.mustCall((err, res) => {
assert.ifError(err);
// Compare copies with ttl removed, c-ares fiddles with that value.
assert.deepStrictEqual(
res.map((r) => Object.assign({}, r, { ttl: null })),
answers.map((r) => Object.assign({}, r, { ttl: null })));
server.close();
}));
}));

0 comments on commit 9c1e270

Please sign in to comment.