Skip to content

Commit 0418a70

Browse files
committed
test: add non-internet resolveAny tests
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 <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
1 parent d9273ed commit 0418a70

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

test/common/dns.js

+290
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/* eslint-disable required-modules */
2+
'use strict';
3+
4+
// Naïve DNS parser/serializer.
5+
6+
const assert = require('assert');
7+
const os = require('os');
8+
9+
const types = {
10+
A: 1,
11+
AAAA: 28,
12+
NS: 2,
13+
CNAME: 5,
14+
SOA: 6,
15+
PTR: 12,
16+
MX: 15,
17+
TXT: 16,
18+
ANY: 255
19+
};
20+
21+
const classes = {
22+
IN: 1
23+
};
24+
25+
function readDomainFromPacket(buffer, offset) {
26+
assert.ok(offset < buffer.length);
27+
const length = buffer[offset];
28+
if (length === 0) {
29+
return { nread: 1, domain: '' };
30+
} else if ((length & 0xC0) === 0) {
31+
offset += 1;
32+
const chunk = buffer.toString('ascii', offset, offset + length);
33+
// Read the rest of the domain.
34+
const { nread, domain } = readDomainFromPacket(buffer, offset + length);
35+
return {
36+
nread: 1 + length + nread,
37+
domain: domain ? `${chunk}.${domain}` : chunk
38+
};
39+
} else {
40+
// Pointer to another part of the packet.
41+
assert.strictEqual(length & 0xC0, 0xC0);
42+
// eslint-disable-next-line
43+
const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
44+
return {
45+
nread: 2,
46+
domain: readDomainFromPacket(buffer, pointeeOffset)
47+
};
48+
}
49+
}
50+
51+
function parseDNSPacket(buffer) {
52+
assert.ok(buffer.length > 12);
53+
54+
const parsed = {
55+
id: buffer.readUInt16BE(0),
56+
flags: buffer.readUInt16BE(2),
57+
};
58+
59+
const counts = [
60+
['questions', buffer.readUInt16BE(4)],
61+
['answers', buffer.readUInt16BE(6)],
62+
['authorityAnswers', buffer.readUInt16BE(8)],
63+
['additionalRecords', buffer.readUInt16BE(10)]
64+
];
65+
66+
let offset = 12;
67+
for (const [ sectionName, count ] of counts) {
68+
parsed[sectionName] = [];
69+
for (let i = 0; i < count; ++i) {
70+
const { nread, domain } = readDomainFromPacket(buffer, offset);
71+
offset += nread;
72+
73+
const type = buffer.readUInt16BE(offset);
74+
75+
const rr = {
76+
domain,
77+
cls: buffer.readUInt16BE(offset + 2),
78+
};
79+
offset += 4;
80+
81+
for (const name in types) {
82+
if (types[name] === type)
83+
rr.type = name;
84+
}
85+
86+
if (sectionName !== 'questions') {
87+
rr.ttl = buffer.readInt32BE(offset);
88+
const dataLength = buffer.readUInt16BE(offset);
89+
offset += 6;
90+
91+
switch (type) {
92+
case types.A:
93+
assert.strictEqual(dataLength, 4);
94+
rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
95+
`${buffer[offset + 2]}.${buffer[offset + 3]}`;
96+
break;
97+
case types.AAAA:
98+
assert.strictEqual(dataLength, 16);
99+
rr.address = buffer.toString('hex', offset, offset + 16)
100+
.replace(/(.{4}(?!$))/g, '$1:');
101+
break;
102+
case types.TXT:
103+
{
104+
let position = offset;
105+
rr.entries = [];
106+
while (position < offset + dataLength) {
107+
const txtLength = buffer[offset];
108+
rr.entries.push(buffer.toString('utf8',
109+
position + 1,
110+
position + 1 + txtLength));
111+
position += 1 + txtLength;
112+
}
113+
assert.strictEqual(position, offset + dataLength);
114+
break;
115+
}
116+
case types.MX:
117+
{
118+
rr.priority = buffer.readInt16BE(buffer, offset);
119+
offset += 2;
120+
const { nread, domain } = readDomainFromPacket(buffer, offset);
121+
rr.exchange = domain;
122+
assert.strictEqual(nread, dataLength);
123+
break;
124+
}
125+
case types.NS:
126+
case types.CNAME:
127+
case types.PTR:
128+
{
129+
const { nread, domain } = readDomainFromPacket(buffer, offset);
130+
rr.value = domain;
131+
assert.strictEqual(nread, dataLength);
132+
break;
133+
}
134+
case types.SOA:
135+
{
136+
const mname = readDomainFromPacket(buffer, offset);
137+
const rname = readDomainFromPacket(buffer, offset + mname.nread);
138+
rr.nsname = mname.domain;
139+
rr.hostmaster = rname.domain;
140+
const trailerOffset = offset + mname.nread + rname.nread;
141+
rr.serial = buffer.readUInt32BE(trailerOffset);
142+
rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
143+
rr.retry = buffer.readUInt32BE(trailerOffset + 8);
144+
rr.expire = buffer.readUInt32BE(trailerOffset + 12);
145+
rr.minttl = buffer.readUInt32BE(trailerOffset + 16);
146+
147+
assert.strictEqual(trailerOffset + 20, dataLength);
148+
break;
149+
}
150+
default:
151+
throw new Error(`Unknown RR type ${rr.type}`);
152+
}
153+
offset += dataLength;
154+
}
155+
156+
parsed[sectionName].push(rr);
157+
158+
assert.ok(offset <= buffer.length);
159+
}
160+
}
161+
162+
assert.strictEqual(offset, buffer.length);
163+
return parsed;
164+
}
165+
166+
function writeIPv6(ip) {
167+
const parts = ip.replace(/^:|:$/g, '').split(':');
168+
const buf = Buffer.alloc(16);
169+
170+
let offset = 0;
171+
for (const part of parts) {
172+
if (part === '') {
173+
offset += 16 - 2 * (parts.length - 1);
174+
} else {
175+
buf.writeUInt16BE(parseInt(part, 16), offset);
176+
offset += 2;
177+
}
178+
}
179+
180+
return buf;
181+
}
182+
183+
function writeDomainName(domain) {
184+
return Buffer.concat(domain.split('.').map((label) => {
185+
assert(label.length < 64);
186+
return Buffer.concat([
187+
Buffer.from([label.length]),
188+
Buffer.from(label, 'ascii')
189+
]);
190+
}).concat([Buffer.alloc(1)]));
191+
}
192+
193+
function writeDNSPacket(parsed) {
194+
const buffers = [];
195+
const kStandardResponseFlags = 0x8180;
196+
197+
buffers.push(new Uint16Array([
198+
parsed.id,
199+
parsed.flags === undefined ? kStandardResponseFlags : parsed.flags,
200+
parsed.questions && parsed.questions.length,
201+
parsed.answers && parsed.answers.length,
202+
parsed.authorityAnswers && parsed.authorityAnswers.length,
203+
parsed.additionalRecords && parsed.additionalRecords.length,
204+
]));
205+
206+
for (const q of parsed.questions) {
207+
assert(types[q.type]);
208+
buffers.push(writeDomainName(q.domain));
209+
buffers.push(new Uint16Array([
210+
types[q.type],
211+
q.cls === undefined ? classes.IN : q.cls
212+
]));
213+
}
214+
215+
for (const rr of [].concat(parsed.answers,
216+
parsed.authorityAnswers,
217+
parsed.additionalRecords)) {
218+
if (!rr) continue;
219+
220+
assert(types[rr.type]);
221+
buffers.push(writeDomainName(rr.domain));
222+
buffers.push(new Uint16Array([
223+
types[rr.type],
224+
rr.cls === undefined ? classes.IN : rr.cls
225+
]));
226+
buffers.push(new Int32Array([rr.ttl]));
227+
228+
const rdLengthBuf = new Uint16Array(1);
229+
buffers.push(rdLengthBuf);
230+
231+
switch (rr.type) {
232+
case 'A':
233+
rdLengthBuf[0] = 4;
234+
buffers.push(new Uint8Array(rr.address.split('.')));
235+
break;
236+
case 'AAAA':
237+
rdLengthBuf[0] = 16;
238+
buffers.push(writeIPv6(rr.address));
239+
break;
240+
case 'TXT':
241+
const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
242+
// Total length of all strings + 1 byte each for their lengths.
243+
rdLengthBuf[0] = rr.entries.length + total;
244+
for (const txt of rr.entries) {
245+
buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
246+
buffers.push(Buffer.from(txt));
247+
}
248+
break;
249+
case 'MX':
250+
rdLengthBuf[0] = 2;
251+
buffers.push(new Uint16Array([rr.priority]));
252+
// fall through
253+
case 'NS':
254+
case 'CNAME':
255+
case 'PTR':
256+
{
257+
const domain = writeDomainName(rr.exchange || rr.value);
258+
rdLengthBuf[0] += domain.length;
259+
buffers.push(domain);
260+
break;
261+
}
262+
case 'SOA':
263+
{
264+
const mname = writeDomainName(rr.nsname);
265+
const rname = writeDomainName(rr.hostmaster);
266+
rdLengthBuf[0] = mname.length + rname.length + 20;
267+
buffers.push(mname, rname);
268+
buffers.push(new Uint32Array([
269+
rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl
270+
]));
271+
break;
272+
}
273+
default:
274+
throw new Error(`Unknown RR type ${rr.type}`);
275+
}
276+
}
277+
278+
return Buffer.concat(buffers.map((typedArray) => {
279+
const buf = Buffer.from(typedArray.buffer,
280+
typedArray.byteOffset,
281+
typedArray.byteLength);
282+
if (os.endianness() === 'LE') {
283+
if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
284+
if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
285+
}
286+
return buf;
287+
}));
288+
}
289+
290+
module.exports = { types, classes, writeDNSPacket, parseDNSPacket };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
const common = require('../common');
3+
const dnstools = require('../common/dns');
4+
const dns = require('dns');
5+
const assert = require('assert');
6+
const dgram = require('dgram');
7+
8+
const server = dgram.createSocket('udp4');
9+
10+
server.on('message', common.mustCall((msg, { address, port }) => {
11+
const parsed = dnstools.parseDNSPacket(msg);
12+
const domain = parsed.questions[0].domain;
13+
assert.strictEqual(domain, 'example.org');
14+
15+
const buf = dnstools.writeDNSPacket({
16+
id: parsed.id,
17+
questions: parsed.questions,
18+
answers: { type: 'A', address: '1.2.3.4', ttl: 123, domain },
19+
});
20+
// Overwrite the # of answers with 2, which is incorrect.
21+
buf.writeUInt16LE(2, 6);
22+
server.send(buf, port, address);
23+
}));
24+
25+
server.bind(0, common.mustCall(() => {
26+
const address = server.address();
27+
dns.setServers([`127.0.0.1:${address.port}`]);
28+
29+
dns.resolveAny('example.org', common.mustCall((err) => {
30+
assert.strictEqual(err.code, 'EBADRESP');
31+
assert.strictEqual(err.syscall, 'queryAny');
32+
assert.strictEqual(err.hostname, 'example.org');
33+
server.close();
34+
}));
35+
}));

test/parallel/test-dns-resolveany.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
const common = require('../common');
3+
const dnstools = require('../common/dns');
4+
const dns = require('dns');
5+
const assert = require('assert');
6+
const dgram = require('dgram');
7+
8+
const answers = [
9+
{ type: 'A', address: '1.2.3.4', ttl: 123 },
10+
{ type: 'AAAA', address: '::42', ttl: 123 },
11+
{ type: 'MX', priority: 42, exchange: 'foobar.com', ttl: 124 },
12+
{ type: 'NS', value: 'foobar.org', ttl: 457 },
13+
{ type: 'TXT', entries: [ 'v=spf1 ~all', 'xyz' ] },
14+
{ type: 'PTR', value: 'baz.org', ttl: 987 },
15+
{
16+
type: 'SOA',
17+
nsname: 'ns1.example.com',
18+
hostmaster: 'admin.example.com',
19+
serial: 156696742,
20+
refresh: 900,
21+
retry: 900,
22+
expire: 1800,
23+
minttl: 60
24+
},
25+
];
26+
27+
const server = dgram.createSocket('udp4');
28+
29+
server.on('message', common.mustCall((msg, { address, port }) => {
30+
const parsed = dnstools.parseDNSPacket(msg);
31+
const domain = parsed.questions[0].domain;
32+
assert.strictEqual(domain, 'example.org');
33+
34+
server.send(dnstools.writeDNSPacket({
35+
id: parsed.id,
36+
questions: parsed.questions,
37+
answers: answers.map((answer) => Object.assign({ domain }, answer)),
38+
}), port, address);
39+
}));
40+
41+
server.bind(0, common.mustCall(() => {
42+
const address = server.address();
43+
dns.setServers([`127.0.0.1:${address.port}`]);
44+
45+
dns.resolveAny('example.org', common.mustCall((err, res) => {
46+
assert.ifError(err);
47+
// Compare copies with ttl removed, c-ares fiddles with that value.
48+
assert.deepStrictEqual(
49+
res.map((r) => Object.assign({}, r, { ttl: null })),
50+
answers.map((r) => Object.assign({}, r, { ttl: null })));
51+
server.close();
52+
}));
53+
}));

0 commit comments

Comments
 (0)