// Local UDP DNS server that answers every query with TC=1 (truncated) so // c-ares retries over TCP on the same port, plus a TCP DNS server that // returns a single TXT "hello" record. The Resolver query then exercises // FilePoll with both .poll_readable and .poll_writable set on one fd. import dgram from "node:dgram"; import net from "node:net"; import { once } from "node:events"; import { Resolver } from "node:dns"; // Build a DNS response that echoes the question section from `query`. function buildResponse(query: Buffer, flagsHi: number, flagsLo: number, answer?: Buffer): Buffer { // Walk labels from offset 12 until the zero-length root label, then // QTYPE(2) + QCLASS(2). let off = 12; while (off < query.length && query[off] !== 0) off += query[off] + 1; off += 1 + 2 + 2; const question = query.subarray(12, off); const header = Buffer.alloc(12); header[0] = query[0]; header[1] = query[1]; header[2] = flagsHi; header[3] = flagsLo; header[5] = 1; // QDCOUNT header[7] = answer ? 1 : 0; // ANCOUNT return answer ? Buffer.concat([header, question, answer]) : Buffer.concat([header, question]); } // A single TXT RR "hello" using the 0xc00c compression pointer back to the // question name at offset 12. const txtAnswer = Buffer.from([ 0xc0, 0x0c, // NAME 0x00, 0x10, // TYPE = TXT 0x00, 0x01, // CLASS = IN 0x00, 0x00, 0x00, 0x3c, // TTL = 60 0x00, 0x06, // RDLENGTH 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, // <5>"hello" ]); // DNS-over-TCP frames are prefixed with a 2-byte big-endian length. function onTcpConnection(socket: net.Socket) { let buf = Buffer.alloc(0); socket.on("data", chunk => { buf = Buffer.concat([buf, chunk]); while (buf.length >= 2) { const len = buf.readUInt16BE(0); if (buf.length < 2 + len) break; const query = buf.subarray(2, 2 + len); buf = buf.subarray(2 + len); // QR=1 RD=1 | RA=1 const resp = buildResponse(query, 0x81, 0x80, txtAnswer); const framed = Buffer.alloc(2 + resp.length); framed.writeUInt16BE(resp.length, 0); resp.copy(framed, 2); socket.write(framed); } }); socket.on("error", () => {}); } // c-ares talks to one server address on both UDP and TCP, so both listeners // must share a port. Bind UDP on an ephemeral port and then try TCP on the // same number; the kernel only guarantees that port was free for UDP, so if // TCP is already taken (EADDRINUSE) drop both and pick another. async function bindPair(): Promise<{ udp: dgram.Socket; tcp: net.Server; port: number }> { let lastErr: unknown; for (let i = 0; i < 32; i++) { const udp = dgram.createSocket("udp4"); udp.bind(0, "127.0.0.1"); await once(udp, "listening"); const port = (udp.address() as dgram.AddressInfo).port; const tcp = net.createServer(onTcpConnection); try { await new Promise((resolve, reject) => { tcp.once("error", reject); tcp.listen(port, "127.0.0.1", resolve); }); } catch (e) { lastErr = e; udp.close(); tcp.close(); if ((e as NodeJS.ErrnoException).code === "EADDRINUSE") continue; throw e; } return { udp, tcp, port }; } throw lastErr; } const { udp, tcp, port } = await bindPair(); udp.on("message", (msg, rinfo) => { // QR=1 Opcode=0 AA=0 TC=1 RD=1 | RA=1 Z=0 RCODE=0 udp.send(buildResponse(msg, 0x83, 0x80), rinfo.port, rinfo.address); }); const r = new Resolver({ timeout: 1000, tries: 1 }); r.setServers(["127.0.0.1:" + port]); r.resolveTxt("example.test", (err, records) => { tcp.close(); udp.close(); if (err) { console.error("resolve error:", (err as NodeJS.ErrnoException).code || err.message); process.exit(1); } console.log(JSON.stringify(records)); process.exit(0); });