UNPKG

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
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; }