#!/usr/bin/env node /** * Minimal DNS server for plaza.tkmind.cn -> 127.0.0.1 * Works with /etc/resolver/plaza.tkmind.cn (macOS per-domain DNS override). * * Usage: * node scripts/plaza-local-dns-server.mjs */ import dgram from 'node:dgram'; const host = process.env.PLAZA_LOCAL_HOST ?? 'plaza.tkmind.cn'; const ip = (process.env.PLAZA_LOCAL_IP ?? '127.0.0.1').split('.').map(Number); const port = Number(process.env.PLAZA_LOCAL_DNS_PORT ?? 5533); const bind = process.env.PLAZA_LOCAL_DNS_BIND ?? '127.0.0.1'; function encodeName(name) { const parts = name.split('.').filter(Boolean); return Buffer.concat([ ...parts.map((part) => Buffer.concat([Buffer.from([part.length]), Buffer.from(part, 'ascii')])), Buffer.from([0]), ]); } const hostEncoded = encodeName(host); function readQuestionName(msg, offset) { const labels = []; let pos = offset; while (pos < msg.length) { const len = msg[pos]; if (len === 0) { pos += 1; break; } labels.push(msg.subarray(pos + 1, pos + 1 + len).toString('ascii')); pos += 1 + len; } return { name: labels.join('.'), next: pos }; } function questionMatches(name) { return name === host || name === `${host}.`; } const server = dgram.createSocket('udp4'); server.on('message', (msg, rinfo) => { if (msg.length < 12) return; const qdCount = msg.readUInt16BE(4); if (qdCount !== 1) return; const question = readQuestionName(msg, 12); const qtype = msg.readUInt16BE(question.next); const qclass = msg.readUInt16BE(question.next + 2); const questionEnd = question.next + 4; if (!questionMatches(question.name) || (qtype !== 1 && qtype !== 28) || qclass !== 1) { return; } const questionSection = msg.subarray(12, questionEnd); const ttl = Buffer.from([0x00, 0x00, 0x00, 0x3c]); // 60s let answer; if (qtype === 1) { answer = Buffer.concat([ Buffer.from([0xc0, 0x0c]), // pointer to question name Buffer.from([0x00, 0x01, 0x00, 0x01]), // A, IN ttl, Buffer.from([0x00, 0x04]), Buffer.from(ip), ]); } else { // AAAA for ::ffff:127.0.0.1 so browsers don't prefer Tailscale fake-ip v6 answer = Buffer.concat([ Buffer.from([0xc0, 0x0c]), Buffer.from([0x00, 0x1c, 0x00, 0x01]), // AAAA, IN ttl, Buffer.from([0x00, 0x10]), Buffer.from([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, ip[0], ip[1], ip[2], ip[3], ]), ]); } const header = Buffer.alloc(12); header.writeUInt16BE(msg.readUInt16BE(0), 0); // ID header.writeUInt16BE(0x8180, 2); // QR=1, AA=1, RD supported header.writeUInt16BE(1, 4); // QDCOUNT header.writeUInt16BE(1, 6); // ANCOUNT header.writeUInt16BE(0, 8); header.writeUInt16BE(0, 10); const response = Buffer.concat([header, questionSection, answer]); server.send(response, rinfo.port, rinfo.address); }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`Plaza DNS 端口 ${port} 已被占用`); process.exit(1); } throw err; }); server.bind(port, bind, () => { console.log(`Plaza DNS ${host} -> ${ip.join('.')} @ ${bind}:${port}`); }); for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => { server.close(() => process.exit(0)); }); }