-
-
Save c0nrad/d464cbb70b1923b6f36723148926e350 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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