Skip to content

Instantly share code, notes, and snippets.

@c0nrad
Created March 11, 2026 17:33
Show Gist options
  • Select an option

  • Save c0nrad/d464cbb70b1923b6f36723148926e350 to your computer and use it in GitHub Desktop.

Select an option

Save c0nrad/d464cbb70b1923b6f36723148926e350 to your computer and use it in GitHub Desktop.
import { Buffer } from "node:buffer";
import { createSocket } from "node:dgram";
// Create the message
interface DnsHeader {
id: number; // 16
qr: number; // 1
opcode: number; // 4
aa: number; // 1
tc: number; // 1
rd: number; // 1
ra: number; // 1
z: number; // 3
rcode: number; // 4
qdcount: number; // 16
ancount: number; // 16
nscount: number; // 16
arcount: number; // 16
}
const header: DnsHeader = {
id: 1337,
qr: 0, // query
opcode: 0, // standard query
rd: 1, // recursion desired
qdcount: 1, // question count
// unused
aa: 0,
tc: 0,
ra: 0,
z: 0,
rcode: 0,
ancount: 0,
nscount: 0,
arcount: 0,
};
function encodeDnsName(domain: string): Buffer {
const labels = domain.split(".");
const parts = labels.map((label) => {
const labelBytes = Buffer.from(label, "ascii");
return Buffer.concat([Buffer.from([labelBytes.length]), labelBytes]);
});
return Buffer.concat([...parts, Buffer.from([0])]);
}
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ID |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QDCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ANCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | NSCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1
const headerBytes = Buffer.alloc(12);
const flags =
(header.qr << 15) |
(header.opcode << 11) |
(header.aa << 10) |
(header.tc << 9) |
(header.rd << 8) |
(header.ra << 7) |
(header.z << 4) |
header.rcode;
headerBytes.writeUInt16BE(header.id, 0);
headerBytes.writeUInt16BE(flags, 2);
headerBytes.writeUInt16BE(header.qdcount, 4);
headerBytes.writeUInt16BE(header.ancount, 6);
headerBytes.writeUInt16BE(header.nscount, 8);
headerBytes.writeUInt16BE(header.arcount, 10);
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.2
// question
// 0 1 2 3 4 5 6 7
// 6 c 0 n r a d 2
// i o 0
const qname = encodeDnsName("c0nrad.io");
const questionBytes = Buffer.alloc(qname.length + 4);
qname.copy(questionBytes, 0);
questionBytes.writeUInt16BE(1, qname.length); // A qtype
questionBytes.writeUInt16BE(1, qname.length + 2); // IN qclass
const message = Buffer.concat([headerBytes, questionBytes]);
const client = createSocket("udp4");
const timeout = setTimeout(() => {
console.error("timed out waiting for DNS response");
client.close();
}, 2_000);
client.once("message", (msg, rinfo) => {
clearTimeout(timeout);
console.log(
`Received ${msg.length} bytes from ${rinfo.address}:${rinfo.port}`,
);
console.log(msg);
parseResponse(msg);
client.close();
});
client.once("error", (err) => {
clearTimeout(timeout);
console.error("socket error", err);
client.close();
});
client.send(message, 53, "8.8.8.8", (err) => {
if (err) {
clearTimeout(timeout);
console.error("send failed", err);
client.close();
}
});
// Parse response
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.3
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | |
// / /
// / NAME /
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TYPE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | CLASS |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | TTL |
// | |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | RDLENGTH |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
// / RDATA /
// / /
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// +---------------------+
// | Header |
// +---------------------+
// | Question | the question for the name server
// +---------------------+
// | Answer | RRs answering the question
// +---------------------+
// | Authority | RRs pointing toward an authority
// +---------------------+
// | Additional | RRs holding additional information
// +---------------------+
function parseResponse(response: Buffer) {
// header
const id = response.readUInt16BE(0);
const flags = response.readUInt16BE(2);
const qdcount = response.readUInt16BE(4);
const ancount = response.readUInt16BE(6);
const nscount = response.readUInt16BE(8);
const arcount = response.readUInt16BE(10);
console.log({ id, flags, qdcount, ancount, nscount, arcount });
// question
let offset = 12; // start of question section
for (let i = 0; i < qdcount; i++) {
const { name, bytesRead } = decodeDnsName(response, offset);
const qtype = response.readUInt16BE(offset + bytesRead);
const qclass = response.readUInt16BE(offset + bytesRead + 2);
console.log({ name, qtype, qclass });
offset += bytesRead + 4;
}
// answers
let answerOffset = offset;
for (let i = 0; i < ancount; i++) {
const { name, bytesRead } = decodeDnsName(response, answerOffset);
const type = response.readUInt16BE(answerOffset + bytesRead);
const class_ = response.readUInt16BE(answerOffset + bytesRead + 2);
const ttl = response.readUInt32BE(answerOffset + bytesRead + 4);
const rdlength = response.readUInt16BE(answerOffset + bytesRead + 8);
const rdata = response.slice(
answerOffset + bytesRead + 10,
answerOffset + bytesRead + 10 + rdlength,
);
if (type !== 1 || class_ !== 1) {
console.warn(`Skipping non-A record: type=${type} class=${class_}`);
answerOffset += bytesRead + 10 + rdlength;
continue;
}
// parse as IP address
const ipAddress = parseIPAddress(rdata);
console.log({ name, type, class_, ttl, rdata, ipAddress });
answerOffset += bytesRead + 10 + rdlength;
}
}
function parseIPAddress(rdata: Buffer): string {
if (rdata.length !== 4) {
throw new Error(`Invalid A record RDATA length: ${rdata.length}`);
}
return Array.from(rdata).join(".");
}
function decodeDnsName(
buffer: Buffer,
offset: number,
): { name: string; bytesRead: number } {
const labels: string[] = [];
let bytesRead = 0;
while (true) {
const length = buffer.readUInt8(offset + bytesRead);
if (length === 0) {
bytesRead += 1; // null terminator
break;
}
if ((length & 0xc0) === 0xc0) {
// Pointer Message Compression
// https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4
const pointer = buffer.readUInt16BE(offset + bytesRead) & 0x3fff;
const pointedName = decodeDnsName(buffer, pointer);
labels.push(pointedName.name);
bytesRead += 2; // pointer is 2 bytes
break;
}
const label = buffer.toString(
"ascii",
offset + bytesRead + 1,
offset + bytesRead + 1 + length,
);
labels.push(label);
bytesRead += length + 1;
}
return { name: labels.join("."), bytesRead };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment