@gajanan_107/dns3 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +17 -0
- package/package.json +13 -0
- package/packet/encoder.js +89 -0
- package/packet/header.js +34 -0
- package/packet/nameCodec.js +47 -0
- package/packet/parser.js +96 -0
- package/packet/records/index.js +147 -0
- package/resolver/forwarder.js +37 -0
- package/resolver/index.js +25 -0
- package/server/udpServer.js +70 -0
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { createServer } = require("./server/udpServer");
|
|
2
|
+
const { parseRequest,
|
|
3
|
+
parseResponse } = require("./packet/parser");
|
|
4
|
+
const { createResponseFromRequest} = require("./resolver/index");
|
|
5
|
+
const { upstreamResponse } = require("./resolver/forwarder");
|
|
6
|
+
const { getRecord,
|
|
7
|
+
registerRecord } = require("./packet/records");
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
createServer,
|
|
11
|
+
parseRequest,
|
|
12
|
+
parseResponse,
|
|
13
|
+
createResponseFromRequest,
|
|
14
|
+
upstreamResponse,
|
|
15
|
+
getRecord,
|
|
16
|
+
registerRecord,
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gajanan_107/dns3",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"type": "commonjs"
|
|
13
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const { buildHeader } = require("./header");
|
|
2
|
+
const { encodeName } = require("./nameCodec");
|
|
3
|
+
const { getRecord } = require("./records");
|
|
4
|
+
|
|
5
|
+
function buildFlagsRaw(flags) {
|
|
6
|
+
return ((flags.qr & 0x1) << 15) |
|
|
7
|
+
((flags.opcode & 0xF) << 11) |
|
|
8
|
+
((flags.aa & 0x1) << 10) |
|
|
9
|
+
((flags.tc & 0x1) << 9) |
|
|
10
|
+
((flags.rd & 0x1) << 8) |
|
|
11
|
+
((flags.ra & 0x1) << 7) |
|
|
12
|
+
((flags.z & 0x1) << 6) |
|
|
13
|
+
(flags.rcode & 0xF);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveHandler(type, context) {
|
|
17
|
+
const handler = getRecord(type);
|
|
18
|
+
if (!handler) throw new Error(`${context}: no handler registered for DNS type "${type}"`);
|
|
19
|
+
return handler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encodeResponse(response) {
|
|
23
|
+
if (!response || typeof response.id !== "number") {
|
|
24
|
+
throw new Error("encodeResponse: response must be an object with an id — use createResponseFromRequest()");
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(response.questions) || response.questions.length === 0) {
|
|
27
|
+
throw new Error("encodeResponse: response must have at least one question");
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(response.answers)) {
|
|
30
|
+
throw new Error("encodeResponse: response.answers must be an array");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const flagsRaw = (response.flags && typeof response.flags === "object")
|
|
34
|
+
? buildFlagsRaw(response.flags)
|
|
35
|
+
: (response.flags || 0x8180);
|
|
36
|
+
|
|
37
|
+
const header = buildHeader({
|
|
38
|
+
id: response.id,
|
|
39
|
+
flags: flagsRaw,
|
|
40
|
+
qdcount: response.questions.length,
|
|
41
|
+
ancount: response.answers.length,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const questionBuffers = response.questions.map((q) => {
|
|
45
|
+
const handler = resolveHandler(q.type, "encodeResponse question");
|
|
46
|
+
const name = encodeName(q.name);
|
|
47
|
+
const typeBuf = Buffer.alloc(2); typeBuf.writeUInt16BE(handler.typeCode);
|
|
48
|
+
const clsBuf = Buffer.alloc(2); clsBuf.writeUInt16BE(q.class);
|
|
49
|
+
return Buffer.concat([name, typeBuf, clsBuf]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const q = response.questions[0];
|
|
53
|
+
|
|
54
|
+
const answerBuffers = response.answers.flatMap((entry, index) => {
|
|
55
|
+
const isFullRecord = entry !== null &&
|
|
56
|
+
typeof entry === "object" &&
|
|
57
|
+
typeof entry.type === "string" &&
|
|
58
|
+
entry.data !== undefined;
|
|
59
|
+
|
|
60
|
+
const record = isFullRecord ? entry : {
|
|
61
|
+
name: q.name,
|
|
62
|
+
type: q.type,
|
|
63
|
+
class: q.class,
|
|
64
|
+
ttl: 300,
|
|
65
|
+
data: entry,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (!record.name || !record.type || record.data === undefined) {
|
|
69
|
+
throw new Error(`encodeResponse: answers[${index}] could not be resolved — push a data value or a full { name, type, class, ttl, data } record`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handler = resolveHandler(record.type, `encodeResponse answers[${index}]`);
|
|
73
|
+
const entries = Array.isArray(record.data) && handler.typeCode !== 16 ? record.data : [record.data];
|
|
74
|
+
|
|
75
|
+
return entries.map((value) => {
|
|
76
|
+
const rdata = handler.encode(value);
|
|
77
|
+
const nameBuf = encodeName(record.name);
|
|
78
|
+
const typeBuf = Buffer.alloc(2); typeBuf.writeUInt16BE(handler.typeCode);
|
|
79
|
+
const clsBuf = Buffer.alloc(2); clsBuf.writeUInt16BE(record.class || 1);
|
|
80
|
+
const ttlBuf = Buffer.alloc(4); ttlBuf.writeUInt32BE(record.ttl || 300);
|
|
81
|
+
const rdlength = Buffer.alloc(2); rdlength.writeUInt16BE(rdata.length);
|
|
82
|
+
return Buffer.concat([nameBuf, typeBuf, clsBuf, ttlBuf, rdlength, rdata]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return Buffer.concat([header, ...questionBuffers, ...answerBuffers]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { encodeResponse };
|
package/packet/header.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function parseHeader(buffer) {
|
|
2
|
+
return {
|
|
3
|
+
id: buffer.readUInt16BE(0),
|
|
4
|
+
|
|
5
|
+
flags: buffer.readUInt16BE(2),
|
|
6
|
+
|
|
7
|
+
qdcount: buffer.readUInt16BE(4),
|
|
8
|
+
ancount: buffer.readUInt16BE(6),
|
|
9
|
+
nscount: buffer.readUInt16BE(8),
|
|
10
|
+
arcount: buffer.readUInt16BE(10),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildHeader({
|
|
15
|
+
id,
|
|
16
|
+
flags = 0x8180,
|
|
17
|
+
qdcount = 1,
|
|
18
|
+
ancount = 1,
|
|
19
|
+
nscount = 0,
|
|
20
|
+
arcount = 0
|
|
21
|
+
}) {
|
|
22
|
+
const buf = Buffer.alloc(12);
|
|
23
|
+
|
|
24
|
+
buf.writeUInt16BE(id, 0);
|
|
25
|
+
buf.writeUInt16BE(flags, 2);
|
|
26
|
+
buf.writeUInt16BE(qdcount, 4);
|
|
27
|
+
buf.writeUInt16BE(ancount, 6);
|
|
28
|
+
buf.writeUInt16BE(nscount, 8);
|
|
29
|
+
buf.writeUInt16BE(arcount, 10);
|
|
30
|
+
|
|
31
|
+
return buf;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { parseHeader, buildHeader };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
function encodeName(name) {
|
|
3
|
+
const buffers = [];
|
|
4
|
+
|
|
5
|
+
for (const part of name.split(".")) {
|
|
6
|
+
const len = Buffer.alloc(1);
|
|
7
|
+
len.writeUInt8(part.length);
|
|
8
|
+
buffers.push(len);
|
|
9
|
+
buffers.push(Buffer.from(part, "ascii"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
buffers.push(Buffer.from([0x00]));
|
|
13
|
+
return Buffer.concat(buffers);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseName(buffer, offset) {
|
|
17
|
+
const labels = [];
|
|
18
|
+
let jumped = false;
|
|
19
|
+
let originalOffset = offset;
|
|
20
|
+
|
|
21
|
+
while (true) {
|
|
22
|
+
const len = buffer[offset];
|
|
23
|
+
|
|
24
|
+
if ((len & 0xC0) === 0xC0) {
|
|
25
|
+
const pointer = ((len & 0x3F) << 8) | buffer[offset + 1];
|
|
26
|
+
if (!jumped) originalOffset = offset + 2;
|
|
27
|
+
offset = pointer;
|
|
28
|
+
jumped = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (len === 0) {
|
|
33
|
+
offset += 1;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
labels.push(buffer.toString("ascii", offset + 1, offset + 1 + len));
|
|
38
|
+
offset += len + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: labels.join("."),
|
|
43
|
+
offset: jumped ? originalOffset : offset,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { encodeName, parseName };
|
package/packet/parser.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const { parseHeader } = require("./header");
|
|
2
|
+
const { parseName } = require("./nameCodec");
|
|
3
|
+
const { getRecord } = require("./records");
|
|
4
|
+
|
|
5
|
+
function parseFlags(raw) {
|
|
6
|
+
return {
|
|
7
|
+
raw,
|
|
8
|
+
qr: (raw >> 15) & 0x1,
|
|
9
|
+
opcode: (raw >> 11) & 0xF,
|
|
10
|
+
aa: (raw >> 10) & 0x1,
|
|
11
|
+
tc: (raw >> 9) & 0x1,
|
|
12
|
+
rd: (raw >> 8) & 0x1,
|
|
13
|
+
ra: (raw >> 7) & 0x1,
|
|
14
|
+
z: (raw >> 6) & 0x1,
|
|
15
|
+
rcode: raw & 0xF,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveType(numericType) {
|
|
20
|
+
const handler = getRecord(numericType);
|
|
21
|
+
return handler ? handler.typeName : String(numericType);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readQuestions(buffer, offset, count) {
|
|
25
|
+
const questions = [];
|
|
26
|
+
for (let i = 0; i < count; i++) {
|
|
27
|
+
if (offset >= buffer.length) throw new Error("parser: buffer ended while reading questions");
|
|
28
|
+
const { name, offset: after } = parseName(buffer, offset);
|
|
29
|
+
offset = after;
|
|
30
|
+
if (offset + 4 > buffer.length) throw new Error("parser: buffer too short for question type/class");
|
|
31
|
+
questions.push({
|
|
32
|
+
name,
|
|
33
|
+
type: resolveType(buffer.readUInt16BE(offset)),
|
|
34
|
+
class: buffer.readUInt16BE(offset + 2),
|
|
35
|
+
});
|
|
36
|
+
offset += 4;
|
|
37
|
+
}
|
|
38
|
+
return { questions, offset };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readRecords(buffer, offset, count) {
|
|
42
|
+
const records = [];
|
|
43
|
+
for (let i = 0; i < count; i++) {
|
|
44
|
+
if (offset >= buffer.length) break;
|
|
45
|
+
const { name, offset: after } = parseName(buffer, offset);
|
|
46
|
+
offset = after;
|
|
47
|
+
if (offset + 10 > buffer.length) throw new Error("parser: buffer too short for resource record");
|
|
48
|
+
const numericType = buffer.readUInt16BE(offset);
|
|
49
|
+
const cls = buffer.readUInt16BE(offset + 2);
|
|
50
|
+
const ttl = buffer.readUInt32BE(offset + 4);
|
|
51
|
+
const rdlength = buffer.readUInt16BE(offset + 8);
|
|
52
|
+
offset += 10;
|
|
53
|
+
if (offset + rdlength > buffer.length) throw new Error("parser: buffer too short for rdata");
|
|
54
|
+
const handler = getRecord(numericType);
|
|
55
|
+
const data = handler ? handler.decode(buffer, offset, rdlength) : buffer.slice(offset, offset + rdlength);
|
|
56
|
+
offset += rdlength;
|
|
57
|
+
records.push({ name, type: resolveType(numericType), class: cls, ttl, data });
|
|
58
|
+
}
|
|
59
|
+
return { records, offset };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRequest(buffer) {
|
|
63
|
+
if (!Buffer.isBuffer(buffer)) throw new Error("parseRequest: expected a raw Buffer");
|
|
64
|
+
const header = parseHeader(buffer);
|
|
65
|
+
const { questions, offset: o1 } = readQuestions(buffer, 12, header.qdcount);
|
|
66
|
+
const { records: answers, offset: o2 } = readRecords(buffer, o1, header.ancount);
|
|
67
|
+
const { records: authority, offset: o3 } = readRecords(buffer, o2, header.nscount);
|
|
68
|
+
const { records: additional } = readRecords(buffer, o3, header.arcount);
|
|
69
|
+
return {
|
|
70
|
+
id: header.id,
|
|
71
|
+
flags: parseFlags(header.flags),
|
|
72
|
+
questions,
|
|
73
|
+
answers,
|
|
74
|
+
authority,
|
|
75
|
+
additional,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseResponse(buffer) {
|
|
80
|
+
if (!Buffer.isBuffer(buffer)) throw new Error("parseResponse: expected a raw Buffer");
|
|
81
|
+
const header = parseHeader(buffer);
|
|
82
|
+
const { questions, offset: o1 } = readQuestions(buffer, 12, header.qdcount);
|
|
83
|
+
const { records: answers, offset: o2 } = readRecords(buffer, o1, header.ancount);
|
|
84
|
+
const { records: authority, offset: o3 } = readRecords(buffer, o2, header.nscount);
|
|
85
|
+
const { records: additional } = readRecords(buffer, o3, header.arcount);
|
|
86
|
+
return {
|
|
87
|
+
id: header.id,
|
|
88
|
+
flags: parseFlags(header.flags),
|
|
89
|
+
questions,
|
|
90
|
+
answers,
|
|
91
|
+
authority,
|
|
92
|
+
additional,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { parseRequest, parseResponse };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { encodeName, parseName } = require("../nameCodec");
|
|
2
|
+
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
|
|
5
|
+
const types = [
|
|
6
|
+
{
|
|
7
|
+
typeCode: 1, typeName: "A",
|
|
8
|
+
encode(ip) {
|
|
9
|
+
const octets = ip.split(".").map(Number);
|
|
10
|
+
if (octets.length !== 4 || octets.some((o) => isNaN(o) || o < 0 || o > 255)) {
|
|
11
|
+
throw new Error(`A record: invalid IPv4 address "${ip}"`);
|
|
12
|
+
}
|
|
13
|
+
return Buffer.from(octets);
|
|
14
|
+
},
|
|
15
|
+
decode(buf, offset) {
|
|
16
|
+
if (offset + 4 > buf.length) throw new Error("A record: buffer too short");
|
|
17
|
+
return `${buf[offset]}.${buf[offset+1]}.${buf[offset+2]}.${buf[offset+3]}`;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
typeCode: 2, typeName: "NS",
|
|
22
|
+
encode(ns) { return encodeName(ns); },
|
|
23
|
+
decode(buf, offset) { return parseName(buf, offset).name; },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
typeCode: 5, typeName: "CNAME",
|
|
27
|
+
encode(cname) { return encodeName(cname); },
|
|
28
|
+
decode(buf, offset) { return parseName(buf, offset).name; },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
typeCode: 6, typeName: "SOA",
|
|
32
|
+
encode({ mname, rname, serial, refresh, retry, expire, minimum }) {
|
|
33
|
+
for (const [k, v] of Object.entries({ mname, rname, serial, refresh, retry, expire, minimum })) {
|
|
34
|
+
if (v === undefined || v === null) throw new Error(`SOA record: missing field "${k}"`);
|
|
35
|
+
}
|
|
36
|
+
const nums = Buffer.alloc(20);
|
|
37
|
+
nums.writeUInt32BE(serial, 0);
|
|
38
|
+
nums.writeUInt32BE(refresh, 4);
|
|
39
|
+
nums.writeUInt32BE(retry, 8);
|
|
40
|
+
nums.writeUInt32BE(expire, 12);
|
|
41
|
+
nums.writeUInt32BE(minimum, 16);
|
|
42
|
+
return Buffer.concat([encodeName(mname), encodeName(rname), nums]);
|
|
43
|
+
},
|
|
44
|
+
decode(buf, offset) {
|
|
45
|
+
const { name: mname, offset: o1 } = parseName(buf, offset);
|
|
46
|
+
const { name: rname, offset: o2 } = parseName(buf, o1);
|
|
47
|
+
if (o2 + 20 > buf.length) throw new Error("SOA record: buffer too short");
|
|
48
|
+
return {
|
|
49
|
+
mname, rname,
|
|
50
|
+
serial: buf.readUInt32BE(o2),
|
|
51
|
+
refresh: buf.readUInt32BE(o2 + 4),
|
|
52
|
+
retry: buf.readUInt32BE(o2 + 8),
|
|
53
|
+
expire: buf.readUInt32BE(o2 + 12),
|
|
54
|
+
minimum: buf.readUInt32BE(o2 + 16),
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
typeCode: 12, typeName: "PTR",
|
|
60
|
+
encode(hostname) { return encodeName(hostname); },
|
|
61
|
+
decode(buf, offset) { return parseName(buf, offset).name; },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
typeCode: 15, typeName: "MX",
|
|
65
|
+
encode({ priority, exchange }) {
|
|
66
|
+
if (typeof priority !== "number") throw new Error("MX record: priority must be a number");
|
|
67
|
+
if (typeof exchange !== "string") throw new Error("MX record: exchange must be a string");
|
|
68
|
+
const prio = Buffer.alloc(2);
|
|
69
|
+
prio.writeUInt16BE(priority);
|
|
70
|
+
return Buffer.concat([prio, encodeName(exchange)]);
|
|
71
|
+
},
|
|
72
|
+
decode(buf, offset) {
|
|
73
|
+
if (offset + 2 > buf.length) throw new Error("MX record: buffer too short");
|
|
74
|
+
const priority = buf.readUInt16BE(offset);
|
|
75
|
+
const { name: exchange } = parseName(buf, offset + 2);
|
|
76
|
+
return { priority, exchange };
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
typeCode: 16, typeName: "TXT",
|
|
81
|
+
encode(value) {
|
|
82
|
+
const strings = Array.isArray(value) ? value : [value];
|
|
83
|
+
return Buffer.concat(strings.map((s) => {
|
|
84
|
+
const content = Buffer.from(s, "ascii");
|
|
85
|
+
if (content.length > 255) throw new Error(`TXT record: string exceeds 255 bytes: "${s}"`);
|
|
86
|
+
const len = Buffer.alloc(1);
|
|
87
|
+
len.writeUInt8(content.length);
|
|
88
|
+
return Buffer.concat([len, content]);
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
decode(buf, offset, rdlength) {
|
|
92
|
+
if (typeof rdlength !== "number") throw new Error("TXT record: rdlength required for decode");
|
|
93
|
+
const strings = [];
|
|
94
|
+
const end = offset + rdlength;
|
|
95
|
+
while (offset < end) {
|
|
96
|
+
const len = buf[offset++];
|
|
97
|
+
strings.push(buf.toString("ascii", offset, offset + len));
|
|
98
|
+
offset += len;
|
|
99
|
+
}
|
|
100
|
+
return strings.length === 1 ? strings[0] : strings;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
typeCode: 28, typeName: "AAAA",
|
|
105
|
+
encode(ip6) {
|
|
106
|
+
const halves = ip6.split("::");
|
|
107
|
+
if (halves.length > 2) throw new Error(`AAAA record: invalid IPv6 address "${ip6}"`);
|
|
108
|
+
const parseGroups = (s) => s ? s.split(":").map((h) => parseInt(h || "0", 16)) : [];
|
|
109
|
+
const left = parseGroups(halves[0]);
|
|
110
|
+
const right = halves.length === 2 ? parseGroups(halves[1]) : [];
|
|
111
|
+
const zeros = new Array(8 - left.length - right.length).fill(0);
|
|
112
|
+
const groups = [...left, ...zeros, ...right];
|
|
113
|
+
if (groups.length !== 8) throw new Error(`AAAA record: invalid IPv6 address "${ip6}"`);
|
|
114
|
+
const buf = Buffer.alloc(16);
|
|
115
|
+
groups.forEach((g, i) => buf.writeUInt16BE(g, i * 2));
|
|
116
|
+
return buf;
|
|
117
|
+
},
|
|
118
|
+
decode(buf, offset) {
|
|
119
|
+
if (offset + 16 > buf.length) throw new Error("AAAA record: buffer too short");
|
|
120
|
+
const groups = [];
|
|
121
|
+
for (let i = 0; i < 8; i++) {
|
|
122
|
+
groups.push(buf.readUInt16BE(offset + i * 2).toString(16));
|
|
123
|
+
}
|
|
124
|
+
return groups.join(":");
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const handler of types) {
|
|
130
|
+
registry.set(handler.typeCode, handler);
|
|
131
|
+
registry.set(handler.typeName.toUpperCase(), handler);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getRecord(typeOrName) {
|
|
135
|
+
const key = typeof typeOrName === "string" ? typeOrName.toUpperCase() : typeOrName;
|
|
136
|
+
return registry.get(key) || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function registerRecord(handler) {
|
|
140
|
+
if (!handler.typeCode || !handler.typeName || typeof handler.encode !== "function" || typeof handler.decode !== "function") {
|
|
141
|
+
throw new Error("registerRecord: handler must have typeCode, typeName, encode(), and decode()");
|
|
142
|
+
}
|
|
143
|
+
registry.set(handler.typeCode, handler);
|
|
144
|
+
registry.set(handler.typeName.toUpperCase(), handler);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { getRecord, registerRecord };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const dgram = require("node:dgram");
|
|
2
|
+
|
|
3
|
+
function upstreamResponse(rawBuffer, upstream = "8.8.8.8", port = 53, timeout = 2000) {
|
|
4
|
+
if (!Buffer.isBuffer(rawBuffer)) {
|
|
5
|
+
throw new Error("upstreamResponse: expected a raw Buffer — pass request._raw or the original msg");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const client = dgram.createSocket("udp4");
|
|
10
|
+
let settled = false;
|
|
11
|
+
|
|
12
|
+
function settle(fn, value) {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
client.close(() => fn(value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
settle(reject, new Error(`upstreamResponse: no reply from ${upstream}:${port} within ${timeout}ms`));
|
|
21
|
+
}, timeout);
|
|
22
|
+
|
|
23
|
+
client.on("error", (err) => {
|
|
24
|
+
settle(reject, new Error(`upstreamResponse: socket error — ${err.message}`));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client.on("message", (msg) => {
|
|
28
|
+
settle(resolve, msg);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
client.send(rawBuffer, port, upstream, (err) => {
|
|
32
|
+
if (err) settle(reject, new Error(`upstreamResponse: failed to send — ${err.message}`));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { upstreamResponse };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function createResponseFromRequest(request) {
|
|
2
|
+
if (!request || typeof request.id !== "number") {
|
|
3
|
+
throw new Error("createResponseFromRequest: pass the object returned by parseRequest()");
|
|
4
|
+
}
|
|
5
|
+
if (!Array.isArray(request.questions) || request.questions.length === 0) {
|
|
6
|
+
throw new Error("createResponseFromRequest: request has no questions");
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
id: request.id,
|
|
10
|
+
flags: {
|
|
11
|
+
qr: 1,
|
|
12
|
+
opcode: request.flags.opcode,
|
|
13
|
+
aa: 0,
|
|
14
|
+
tc: 0,
|
|
15
|
+
rd: request.flags.rd,
|
|
16
|
+
ra: 1,
|
|
17
|
+
z: 0,
|
|
18
|
+
rcode: 0,
|
|
19
|
+
},
|
|
20
|
+
questions: request.questions.map((q) => ({ ...q })),
|
|
21
|
+
answers: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { createResponseFromRequest };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const dgram = require("node:dgram");
|
|
2
|
+
const { encodeResponse } = require("../packet/encoder");
|
|
3
|
+
|
|
4
|
+
function createServer() {
|
|
5
|
+
const socket = dgram.createSocket("udp4");
|
|
6
|
+
const handlers = {};
|
|
7
|
+
|
|
8
|
+
socket.on("message", (msg, rinfo) => {
|
|
9
|
+
const handler = handlers["onmessage"];
|
|
10
|
+
if (!handler) return;
|
|
11
|
+
|
|
12
|
+
function send(response) {
|
|
13
|
+
let buffer;
|
|
14
|
+
|
|
15
|
+
if (Buffer.isBuffer(response)) {
|
|
16
|
+
buffer = response;
|
|
17
|
+
} else {
|
|
18
|
+
buffer = encodeResponse(response);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
socket.send(buffer, rinfo.port, rinfo.address, (err) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
const errHandler = handlers["onerror"];
|
|
24
|
+
if (errHandler) errHandler(new Error(`send failed: ${err.message}`), rinfo);
|
|
25
|
+
else console.error("[dns3] send error:", err.message);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Promise.resolve()
|
|
31
|
+
.then(() => handler(msg, send))
|
|
32
|
+
.catch((err) => {
|
|
33
|
+
const errHandler = handlers["onerror"];
|
|
34
|
+
if (errHandler) errHandler(err, rinfo);
|
|
35
|
+
else console.error("[dns3] unhandled error:", err.message);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
socket.on("error", (err) => {
|
|
40
|
+
const errHandler = handlers["onerror"];
|
|
41
|
+
if (errHandler) errHandler(err, null);
|
|
42
|
+
else console.error("[dns3] socket error:", err.message);
|
|
43
|
+
socket.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
handle(event, fn) {
|
|
48
|
+
const key = event.toLowerCase().replace(/^on/, "on");
|
|
49
|
+
const allowed = ["onmessage", "onerror"];
|
|
50
|
+
if (!allowed.includes(key)) {
|
|
51
|
+
throw new Error(`handle: unknown event "${event}" — supported events: onmessage, onerror`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof fn !== "function") {
|
|
54
|
+
throw new Error(`handle: handler for "${event}" must be a function`);
|
|
55
|
+
}
|
|
56
|
+
handlers[key] = fn;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
listen(port = 5353) {
|
|
60
|
+
if (typeof port !== "number" || port < 1 || port > 65535) {
|
|
61
|
+
throw new Error("listen: port must be a number between 1 and 65535");
|
|
62
|
+
}
|
|
63
|
+
socket.bind(port, () => {
|
|
64
|
+
console.log(`[dns3] listening on port ${port}`);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { createServer };
|