mock-dns-server
Version:
Create a mock DNS-over-TLS server based on [mock-tls-server](https://github.com/hildjj/mock-tls-server/).
140 lines (139 loc) • 4.83 kB
JavaScript
import * as packet from 'dns-packet';
// @ts-expect-error Incomplete types
// eslint-disable-next-line n/no-missing-import
import * as rcodes from 'dns-packet/rcodes.js';
import { MockTLSServer } from 'mock-tls-server';
import { NoFilter } from 'nofilter';
import assert from 'node:assert';
export { connect, plainConnect } from 'mock-tls-server';
// See RFC 8467:
// (2) If a server receives a query that includes the EDNS(0) "Padding"
// option, it MUST pad the corresponding response (see Section 4 of RFC 7830)
// and SHOULD pad the corresponding response to a multiple of 468 octets (see
// below).
const PAD_SIZE = 468;
const AA = 1 << 10;
const CONNECTION = Symbol('connection');
class Connection {
zones;
#sock;
#size;
#nof;
constructor(sock, options) {
const opts = {
zones: {},
...options,
};
this.#sock = sock;
this.#size = -1;
this.#nof = new NoFilter();
this.zones = opts.zones;
this.#sock.on('data', this._data.bind(this));
}
_data(chunk) {
this.#nof.write(chunk);
while (this.#nof.length > 0) {
if (this.#size === -1) {
if (this.#nof.length < 2) {
return;
}
this.#size = this.#nof.readUInt16BE();
}
if (this.#nof.length < this.#size) {
return;
}
const buf = this.#nof.read(this.#size);
this.#size = -1;
const pkt = packet.decode(buf);
let chunky = false;
assert(pkt.id !== undefined);
assert(pkt.questions);
const rp = {
id: pkt.id,
type: 'response',
flags: AA,
questions: pkt.questions,
answers: [],
additionals: [],
authorities: [],
};
for (const { name, type } of pkt.questions) {
if (/badid/i.test(name)) {
rp.id = (pkt.id + 1) % 65536;
}
if (/chunky/.test(name)) {
chunky = true;
}
const domain = this.zones[name];
if (domain) {
const data = [domain[type]].flat();
for (const d of data) {
const ans = {
name,
type,
class: 'IN',
ttl: 1000,
data: d,
};
rp.answers?.push(ans);
}
}
}
if (!rp.answers?.length) {
rp.flags = AA | rcodes.toRcode('NXDOMAIN');
}
// Only pad if client said they support EDNS0 and request padding
const opt = pkt.additionals?.find(a => a.type === 'OPT');
if (opt?.options?.find(o => o.type === 'PADDING')) {
const padding = {
code: 12, // 'PADDING'
length: 0,
};
rp.additionals?.push({
name: '.',
type: 'OPT',
udpPayloadSize: 4096,
flags: 0,
options: [padding],
ednsVersion: 0,
extendedRcode: 0,
flag_do: false,
});
const unpadded = packet.encodingLength(rp);
padding.length =
(Math.ceil(unpadded / PAD_SIZE) * PAD_SIZE) - unpadded;
}
const reply = packet.streamEncode(rp);
if (chunky) {
// Write in chunks, for testing reassembly
// Avoid Nagle by going full-sync
this.#sock.write(reply.subarray(0, 1), () => {
this.#sock.write(reply.subarray(1, 2), () => {
this.#sock.write(reply.subarray(2, 7), () => {
this.#sock.write(reply.subarray(7));
});
});
});
}
else {
this.#sock.write(reply);
}
}
}
}
/**
* Create a mock DNS server.
*
* @param [options] Any options for mock-tls-server. Port defaults
* to 853.
* @returns The created server, already listening.
*/
export function createServer(options = {}) {
const { port = 853, zones = {}, ...opts } = options;
const server = new MockTLSServer(opts);
server.listen(port, (cli) => {
// @ts-expect-error Lifetime of server tied to client
cli[CONNECTION] = new Connection(cli, { zones });
});
return server;
}