dns2
Version:
A DNS Server and Client Implementation in Pure JavaScript with no dependencies.
143 lines (138 loc) • 4.54 kB
JavaScript
const udp = require('node:dgram');
const net = require('node:net');
const Packet = require('../packet');
const { debuglog } = require('node:util');
const debug = debuglog('dns2');
module.exports = ({
dns = '8.8.8.8',
port = 53,
socketType = 'udp4',
timeout = 10000,
retryOverTCP = true,
} = {}) => {
return (name, type = 'A', cls = Packet.CLASS.IN, options = {}) => {
const { clientIp, recursive = true } = options;
const query = new Packet();
query.header.id = Packet.uuid();
// see https://github.com/song940/node-dns/issues/29
if (recursive) {
query.header.rd = 1;
}
if (clientIp) {
query.additionals.push(
Packet.Resource.EDNS([Packet.Resource.EDNS.ECS(clientIp)]),
);
}
query.questions.push({
name,
class: cls,
type: Packet.TYPE[type],
});
const client = new udp.Socket(socketType);
// Only enforce a strict source-address check when `dns` is an IP literal;
// hostnames would require an extra resolve to compare against.
const expectedAddress = net.isIP(dns) ? dns : null;
return new Promise((resolve, reject) => {
let settled = false;
let timer;
// Packets that cannot be decoded are dropped, a forged or corrupt datagram
// must not pre-empt the real reply. Keep the reason so a subsequent timeout
// can explain itself
let lastDropped;
const cleanup = () => {
if (settled) return;
settled = true;
if (timer) clearTimeout(timer);
client.removeListener('message', onMessage);
client.removeListener('error', onError);
try {
client.close();
} catch (_) {
/* already closed */
}
};
function onMessage(message, rinfo) {
// Drop packets that didn't come from the configured resolver.
if (
rinfo.port !== port ||
(expectedAddress && rinfo.address !== expectedAddress)
) {
lastDropped =
`packet came from ${rinfo.address}:${rinfo.port}, not the ` +
`configured resolver ${dns}:${port}`;
debug(
'udp: dropping packet from unexpected sender %s:%d',
rinfo.address,
rinfo.port,
);
return;
}
let response;
try {
response = Packet.parse(message);
} catch (e) {
lastDropped = `response could not be decoded: ${e.message}`;
debug('udp: dropping unparseable packet: %s', e.message);
return;
}
if (response.errors.length) {
debug(
'udp: response %d decoded with %d error(s): %s',
response.header.id,
response.errors.length,
response.errors.map(e => e.message).join('; '),
);
}
// Stray / late reply from a reused ephemeral port — keep listening.
if (response.header.id !== query.header.id) {
lastDropped =
`response id ${response.header.id} did not match the query ` +
`id ${query.header.id}`;
debug(
'udp: dropping response with mismatched id %d (expected %d)',
response.header.id,
query.header.id,
);
return;
}
// RFC 1035 §4.2.1: if the TC (truncated) bit is set the upstream had
// more data than fits in a 512-byte UDP datagram. Retry over TCP so
// callers always receive a complete answer.
if (response.header.tc && retryOverTCP) {
debug('udp: TC bit set — retrying query over TCP');
cleanup();
const TCPClient = require('./tcp');
resolve(TCPClient({ dns, port })(name, type, cls, options));
return;
}
cleanup();
resolve(response);
}
function onError(err) {
cleanup();
reject(err);
}
client.on('message', onMessage);
client.on('error', onError);
if (timeout > 0) {
timer = setTimeout(() => {
cleanup();
const err = new Error(
`DNS query timed out after ${timeout}ms` +
(lastDropped ? ` (last ${lastDropped})` : ''),
);
err.code = 'ETIMEDOUT';
reject(err);
}, timeout);
timer.unref();
}
debug('send', dns, query.toBuffer());
client.send(query.toBuffer(), port, dns, err => {
if (err) {
cleanup();
reject(err);
}
});
});
};
};