WIP: DNS response parsing

This commit is contained in:
2025-01-02 12:03:57 -06:00
parent 41cdaba05c
commit d43a958cfe
7 changed files with 477 additions and 1 deletions

39
dist/dns.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
export declare const TypeTxt = 16;
export declare const TypePtr = 12;
export declare const TypeSrv = 33;
export declare const TypeA = 1;
export declare class Question {
name: string;
type: number;
class: number;
endOffset: number;
constructor(_name: string, _type: number, _cls: number, _endOffset: number);
}
export interface Record {
interface: string | undefined;
type: number;
ttlSeconds: number;
name: string;
endOffset: number;
}
export interface PtrRecord extends Record {
interface: "ptr";
domain: string;
}
export interface TxtRecord extends Record {
interface: "txt";
text: string;
}
export interface SrvRecord extends Record {
interface: "srv";
priority: number;
weight: number;
port: number;
target: string;
}
export interface ARecord extends Record {
interface: "a";
address: string;
}
export declare function GetDNSQuestion(msg: Buffer, startOffset: number): Question;
export declare function GetDNSAnswer(msg: Buffer, startOffset: number): Record | undefined;

146
dist/dns.js vendored Normal file
View File

@ -0,0 +1,146 @@
export const TypeTxt = 16;
export const TypePtr = 12;
export const TypeSrv = 33;
export const TypeA = 1;
export class Question {
name = "";
type = 0;
class = 0;
endOffset = 0;
constructor(_name, _type, _cls, _endOffset) {
this.name = _name;
this.type = _type;
this.class = _cls;
this.endOffset = _endOffset;
}
}
class dnsAnswerParseResult {
name = "";
endOffset = 0;
}
function parseDnsName(msg, startOffset) {
const result = new dnsAnswerParseResult();
let offset = startOffset;
while (offset < msg.length) {
const nextByte = msg.readUInt8(offset);
if (nextByte === 0) {
offset++;
break;
}
const pointerMask = (1 << 6) | (1 << 7);
if ((nextByte & pointerMask) === pointerMask) {
offset++;
const pointerOffset = msg.readUInt8(offset);
offset++;
const nestedResult = parseDnsName(msg, pointerOffset);
if (result.name.length > 0 && nestedResult.name.length > 0) {
result.name += ".";
}
result.name += nestedResult.name;
break;
}
offset++;
const segment = msg.toString("ascii", offset, offset + nextByte);
offset += nextByte;
if (result.name.length > 0 && segment.length > 0) {
result.name += ".";
}
result.name += segment;
}
result.endOffset = offset;
return result;
}
export function GetDNSQuestion(msg, startOffset) {
let offset = startOffset;
const parsedResult = parseDnsName(msg, offset);
offset = parsedResult.endOffset;
const type = msg.readUInt16BE(offset);
offset += 2;
const cls = msg.readUInt16BE(offset);
offset += 2;
return new Question(parsedResult.name, type, cls, offset);
}
export function GetDNSAnswer(msg, startOffset) {
let offset = startOffset;
const parsedResult = parseDnsName(msg, offset);
offset = parsedResult.endOffset;
const type = msg.readUInt16BE(offset);
offset += 2;
offset += 2; // don't care about "class" of answer
const ttlSeconds = msg.readUInt32BE(offset);
offset += 4;
const remainingDataLength = msg.readUInt16BE(offset);
offset += 2;
switch (type) {
case TypePtr: {
const domainResult = parseDnsName(msg, offset);
const ret = {
interface: "ptr",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
domain: domainResult.name,
};
return ret;
}
case TypeTxt: {
const textResult = parseDnsName(msg, offset);
const ret = {
interface: "txt",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
text: textResult.name,
};
return ret;
}
case TypeSrv: {
const priority = msg.readUInt16BE(offset);
offset += 2;
const weight = msg.readUInt16BE(offset);
offset += 2;
const port = msg.readUInt16BE(offset);
offset += 2;
const targetResult = parseDnsName(msg, offset);
offset = targetResult.endOffset;
const ret = {
interface: "srv",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset,
priority: priority,
weight: weight,
port: port,
target: targetResult.name,
};
return ret;
}
case TypeA: {
const o1 = msg.readUInt8(offset);
offset++;
const o2 = msg.readUInt8(offset);
offset++;
const o3 = msg.readUInt8(offset);
offset++;
const o4 = msg.readUInt8(offset);
offset++;
const address = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
const ret = {
interface: "a",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
address: address,
};
return ret;
}
default:
break;
}
return undefined;
}
//# sourceMappingURL=dns.js.map

1
dist/dns.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../dns.ts"],"names":[],"mappings":"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"}

41
dist/finder.js vendored
View File

@ -2,6 +2,7 @@ import * as dgram from "dgram";
import { EventEmitter } from "events";
import { setTimeout as setTimeoutSync } from "timers";
import os from "os";
import { GetDNSAnswer, GetDNSQuestion } from "./dns.js";
export class FindUnits extends EventEmitter {
constructor() {
super();
@ -103,6 +104,46 @@ export class FindUnits extends EventEmitter {
}
foundServer(msg, remote) {
// debugFind("found something");
let flags = 0;
if (msg.length > 4) {
flags = msg.readUInt16BE(2);
const answerBit = 1 << 15;
if ((flags & answerBit) === 0) {
// received query, don't process as answer
return;
}
}
let nextAnswerOffset = 12;
let questions = 0;
if (msg.length >= 6) {
questions = msg.readUInt16BE(4);
let nextQuestionOffset = 12;
for (let i = 0; i < questions; i++) {
const parsed = GetDNSQuestion(msg, nextQuestionOffset);
nextQuestionOffset = parsed.endOffset;
}
nextAnswerOffset = nextQuestionOffset;
}
let answers = 0;
if (msg.length >= 8) {
answers = msg.readUInt16BE(6);
}
if (answers > 0) {
for (let i = 0; i < answers; i++) {
if (msg.length <= nextAnswerOffset) {
console.error(`while inspecting dns answers, expected message length > ${nextAnswerOffset.toString()} but it was ${msg.length.toString()}`);
break;
}
const answer = GetDNSAnswer(msg, nextAnswerOffset);
if (!answer) {
break;
}
nextAnswerOffset = answer.endOffset;
if (answer.interface === "a") {
console.log("a record:", answer);
}
}
}
const str = msg.toString();
console.log(str);
if (msg.length >= 40) {

2
dist/finder.js.map vendored

File diff suppressed because one or more lines are too long

198
dns.ts Normal file
View File

@ -0,0 +1,198 @@
export const TypeTxt = 16;
export const TypePtr = 12;
export const TypeSrv = 33;
export const TypeA = 1;
export class Question {
public name = "";
public type = 0;
public class = 0;
public endOffset = 0;
constructor(_name: string, _type: number, _cls: number, _endOffset: number) {
this.name = _name;
this.type = _type;
this.class = _cls;
this.endOffset = _endOffset;
}
}
export interface Record {
interface: string | undefined;
type: number;
ttlSeconds: number;
name: string;
endOffset: number;
}
export interface PtrRecord extends Record {
interface: "ptr";
domain: string;
}
export interface TxtRecord extends Record {
interface: "txt";
text: string;
}
export interface SrvRecord extends Record {
interface: "srv";
priority: number;
weight: number;
port: number;
target: string;
}
export interface ARecord extends Record {
interface: "a";
address: string;
}
class dnsAnswerParseResult {
public name = "";
public endOffset = 0;
}
function parseDnsName(msg: Buffer, startOffset: number): dnsAnswerParseResult {
const result = new dnsAnswerParseResult();
let offset = startOffset;
while (offset < msg.length) {
const nextByte = msg.readUInt8(offset);
if (nextByte === 0) {
offset++;
break;
}
const pointerMask = (1 << 6) | (1 << 7);
if ((nextByte & pointerMask) === pointerMask) {
offset++;
const pointerOffset = msg.readUInt8(offset);
offset++;
const nestedResult = parseDnsName(msg, pointerOffset);
if (result.name.length > 0 && nestedResult.name.length > 0) {
result.name += ".";
}
result.name += nestedResult.name;
break;
}
offset++;
const segment = msg.toString("ascii", offset, offset + nextByte);
offset += nextByte;
if (result.name.length > 0 && segment.length > 0) {
result.name += ".";
}
result.name += segment;
}
result.endOffset = offset;
return result;
}
export function GetDNSQuestion(msg: Buffer, startOffset: number): Question {
let offset = startOffset;
const parsedResult = parseDnsName(msg, offset);
offset = parsedResult.endOffset;
const type = msg.readUInt16BE(offset);
offset += 2;
const cls = msg.readUInt16BE(offset);
offset += 2;
return new Question(parsedResult.name, type, cls, offset);
}
export function GetDNSAnswer(
msg: Buffer,
startOffset: number,
): Record | undefined {
let offset = startOffset;
const parsedResult = parseDnsName(msg, offset);
offset = parsedResult.endOffset;
const type = msg.readUInt16BE(offset);
offset += 2;
offset += 2; // don't care about "class" of answer
const ttlSeconds = msg.readUInt32BE(offset);
offset += 4;
const remainingDataLength = msg.readUInt16BE(offset);
offset += 2;
switch (type) {
case TypePtr: {
const domainResult = parseDnsName(msg, offset);
const ret: PtrRecord = {
interface: "ptr",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
domain: domainResult.name,
};
return ret;
}
case TypeTxt: {
const textResult = parseDnsName(msg, offset);
const ret: TxtRecord = {
interface: "txt",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
text: textResult.name,
};
return ret;
}
case TypeSrv: {
const priority = msg.readUInt16BE(offset);
offset += 2;
const weight = msg.readUInt16BE(offset);
offset += 2;
const port = msg.readUInt16BE(offset);
offset += 2;
const targetResult = parseDnsName(msg, offset);
offset = targetResult.endOffset;
const ret: SrvRecord = {
interface: "srv",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset,
priority: priority,
weight: weight,
port: port,
target: targetResult.name,
};
return ret;
}
case TypeA: {
const o1 = msg.readUInt8(offset);
offset++;
const o2 = msg.readUInt8(offset);
offset++;
const o3 = msg.readUInt8(offset);
offset++;
const o4 = msg.readUInt8(offset);
offset++;
const address = `${o1.toString()}.${o2.toString()}.${o3.toString()}.${o4.toString()}`;
const ret: ARecord = {
interface: "a",
type: type,
ttlSeconds: ttlSeconds,
name: parsedResult.name,
endOffset: offset + remainingDataLength,
address: address,
};
return ret;
}
default:
break;
}
return undefined;
}

View File

@ -3,6 +3,7 @@ import { Socket } from "dgram";
import { EventEmitter } from "events";
import { setTimeout as setTimeoutSync } from "timers";
import os from "os";
import { GetDNSAnswer, GetDNSQuestion } from "./dns.js";
export class FindUnits extends EventEmitter {
constructor() {
@ -119,6 +120,56 @@ export class FindUnits extends EventEmitter {
foundServer(msg: Buffer, remote: dgram.RemoteInfo) {
// debugFind("found something");
let flags = 0;
if (msg.length > 4) {
flags = msg.readUInt16BE(2);
const answerBit = 1 << 15;
if ((flags & answerBit) === 0) {
// received query, don't process as answer
return;
}
}
let nextAnswerOffset = 12;
let questions = 0;
if (msg.length >= 6) {
questions = msg.readUInt16BE(4);
let nextQuestionOffset = 12;
for (let i = 0; i < questions; i++) {
const parsed = GetDNSQuestion(msg, nextQuestionOffset);
nextQuestionOffset = parsed.endOffset;
}
nextAnswerOffset = nextQuestionOffset;
}
let answers = 0;
if (msg.length >= 8) {
answers = msg.readUInt16BE(6);
}
if (answers > 0) {
for (let i = 0; i < answers; i++) {
if (msg.length <= nextAnswerOffset) {
console.error(
`while inspecting dns answers, expected message length > ${nextAnswerOffset.toString()} but it was ${msg.length.toString()}`,
);
break;
}
const answer = GetDNSAnswer(msg, nextAnswerOffset);
if (!answer) {
break;
}
nextAnswerOffset = answer.endOffset;
if (answer.interface === "a") {
console.log("a record:", answer);
}
}
}
const str = msg.toString();
console.log(str);