@opensecurity/zonzon-core 0.1.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/package.json +37 -0
- package/src/audit.ts +43 -0
- package/src/cache-layer.test.ts +236 -0
- package/src/cache-multi-question.test.ts +263 -0
- package/src/dns-handler.ts +355 -0
- package/src/dns-service.test.ts +371 -0
- package/src/dns-service.ts +655 -0
- package/src/dns-wireformat.test.ts +771 -0
- package/src/env.d.ts +1 -0
- package/src/firewall.ts +66 -0
- package/src/http-body-forwarding-integration.test.ts +357 -0
- package/src/http-body-forwarding.test.ts +101 -0
- package/src/http-handler.ts +489 -0
- package/src/http-proxy.test.ts +440 -0
- package/src/http-proxy.ts +148 -0
- package/src/index.ts +10 -0
- package/src/rate-limiter.test.ts +144 -0
- package/src/rate-limiter.ts +50 -0
- package/src/schema.test.ts +685 -0
- package/src/schema.ts +137 -0
- package/src/sni-proxy.ts +164 -0
- package/src/srv-record.test.ts +211 -0
- package/src/tcp-connection-limit.test.ts +110 -0
- package/src/types.ts +168 -0
- package/src/wildcard-matching.test.ts +196 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import dgram, { RemoteInfo } from "dgram";
|
|
2
|
+
import * as net from "net";
|
|
3
|
+
import { DevDnsServer, DnsWireFormat, extractQuestions } from "./dns-service.js";
|
|
4
|
+
import { ServerConfig, DNS_RCODE } from "./types.js";
|
|
5
|
+
import { RateLimiter } from "./rate-limiter.js";
|
|
6
|
+
import { audit } from "./audit.js";
|
|
7
|
+
import { firewallEngine } from "./firewall.js";
|
|
8
|
+
|
|
9
|
+
const MAX_TCP_BUFFER_SIZE = 64 * 1024;
|
|
10
|
+
|
|
11
|
+
export class DnsHandler {
|
|
12
|
+
private server: DevDnsServer;
|
|
13
|
+
private udpServer: dgram.Socket | null = null;
|
|
14
|
+
private tcpServer: net.Server | null = null;
|
|
15
|
+
private port: number;
|
|
16
|
+
private fallbackDns: string | undefined;
|
|
17
|
+
private config: ServerConfig;
|
|
18
|
+
|
|
19
|
+
private activeTcpConnections = new Map<net.Socket, { timeoutId?: ReturnType<typeof setTimeout> }>();
|
|
20
|
+
private maxTcpConnections: number;
|
|
21
|
+
private tcpIdleTimeoutMs: number;
|
|
22
|
+
|
|
23
|
+
private rateLimiter: RateLimiter | null;
|
|
24
|
+
|
|
25
|
+
constructor(server: DevDnsServer, config: ServerConfig) {
|
|
26
|
+
this.server = server;
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.port = config.port;
|
|
29
|
+
this.fallbackDns = config.fallbackDns;
|
|
30
|
+
this.maxTcpConnections = config.maxTcpConnections ?? 100;
|
|
31
|
+
this.tcpIdleTimeoutMs = config.tcpIdleTimeoutMs ?? 30000;
|
|
32
|
+
|
|
33
|
+
if (config.rateLimitMaxRequests && config.rateLimitMaxRequests > 0) {
|
|
34
|
+
this.rateLimiter = new RateLimiter({
|
|
35
|
+
maxRequests: config.rateLimitMaxRequests,
|
|
36
|
+
windowMs: config.rateLimitWindowMs ?? 1000,
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
this.rateLimiter = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async start(): Promise<void> {
|
|
44
|
+
await this.startUdp();
|
|
45
|
+
await this.startTcp();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async stop(): Promise<void> {
|
|
49
|
+
if (this.udpServer) {
|
|
50
|
+
await new Promise<void>((resolve) => {
|
|
51
|
+
this.udpServer?.close(() => resolve());
|
|
52
|
+
});
|
|
53
|
+
this.udpServer = null;
|
|
54
|
+
}
|
|
55
|
+
if (this.tcpServer) {
|
|
56
|
+
await new Promise<void>((resolve) => {
|
|
57
|
+
this.tcpServer?.close(() => resolve());
|
|
58
|
+
});
|
|
59
|
+
this.tcpServer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private startUdp(): Promise<void> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const udp = dgram.createSocket("udp4");
|
|
66
|
+
|
|
67
|
+
udp.on("message", (data: Buffer, rinfo: RemoteInfo) => {
|
|
68
|
+
this.handleUdpMessage(data, rinfo);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
udp.on("error", (err: NodeJS.ErrnoException) => {
|
|
72
|
+
if (err.code === "EACCES" || err.code === "EADDRINUSE") {
|
|
73
|
+
reject(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
udp.on("listening", () => {
|
|
78
|
+
try { udp.setRecvBufferSize(1024 * 1024); } catch {}
|
|
79
|
+
this.udpServer = udp;
|
|
80
|
+
resolve();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
udp.bind(this.port, "0.0.0.0");
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private startTcp(): Promise<void> {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const tcpServer = net.createServer((socket: net.Socket) => {
|
|
90
|
+
if (this.activeTcpConnections.size >= this.maxTcpConnections) {
|
|
91
|
+
socket.destroy();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.activeTcpConnections.set(socket, { timeoutId: undefined });
|
|
96
|
+
|
|
97
|
+
socket.setTimeout(this.tcpIdleTimeoutMs);
|
|
98
|
+
socket.on("timeout", () => {
|
|
99
|
+
this.removeTcpConnection(socket);
|
|
100
|
+
socket.destroy();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.handleTcpConnection(socket);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
tcpServer.on("close", () => {
|
|
107
|
+
for (const [socket] of this.activeTcpConnections) {
|
|
108
|
+
if (!socket.destroyed) socket.destroy();
|
|
109
|
+
}
|
|
110
|
+
this.activeTcpConnections.clear();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
tcpServer.on("listening", () => {
|
|
114
|
+
this.tcpServer = tcpServer;
|
|
115
|
+
resolve();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
tcpServer.listen(this.port, "0.0.0.0");
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private isRateLimited(ip: string): boolean {
|
|
123
|
+
if (!this.rateLimiter) return false;
|
|
124
|
+
return !this.rateLimiter.allow(ip);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private parseResolvedIpv4s(resp: Buffer): string[] {
|
|
128
|
+
const ips: string[] = [];
|
|
129
|
+
try {
|
|
130
|
+
const f = new DnsWireFormat(resp);
|
|
131
|
+
f.offset = 4;
|
|
132
|
+
const qd = f.readUint16();
|
|
133
|
+
const an = f.readUint16();
|
|
134
|
+
f.offset += 4;
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < qd; i++) {
|
|
137
|
+
f.readDomainName();
|
|
138
|
+
f.offset += 4;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < an; i++) {
|
|
142
|
+
f.readDomainName();
|
|
143
|
+
const type = f.readUint16();
|
|
144
|
+
f.offset += 6;
|
|
145
|
+
const len = f.readUint16();
|
|
146
|
+
if (type === 1 && len === 4) {
|
|
147
|
+
ips.push(`${resp[f.offset]}.${resp[f.offset+1]}.${resp[f.offset+2]}.${resp[f.offset+3]}`);
|
|
148
|
+
}
|
|
149
|
+
f.offset += len;
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
return ips;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private forwardUdpQuery(data: Buffer, clientInfo: RemoteInfo): void {
|
|
156
|
+
if (!this.fallbackDns || !this.udpServer) return;
|
|
157
|
+
|
|
158
|
+
const fwdSocket = dgram.createSocket("udp4");
|
|
159
|
+
let handled = false;
|
|
160
|
+
|
|
161
|
+
const timeout = setTimeout(() => {
|
|
162
|
+
if (!handled) {
|
|
163
|
+
handled = true;
|
|
164
|
+
fwdSocket.close();
|
|
165
|
+
const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
|
|
166
|
+
this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
|
|
167
|
+
}
|
|
168
|
+
}, 3000);
|
|
169
|
+
|
|
170
|
+
fwdSocket.on("message", (resp) => {
|
|
171
|
+
if (!handled) {
|
|
172
|
+
handled = true;
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
|
|
175
|
+
const ips = this.parseResolvedIpv4s(resp);
|
|
176
|
+
let blockedIp = null;
|
|
177
|
+
|
|
178
|
+
for (const ip of ips) {
|
|
179
|
+
if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
|
|
180
|
+
blockedIp = ip;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (blockedIp) {
|
|
186
|
+
const ref = this.server.generateErrorResponse(data, DNS_RCODE.REFUSED);
|
|
187
|
+
this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
|
|
188
|
+
} else {
|
|
189
|
+
this.udpServer?.send(resp, 0, resp.length, clientInfo.port, clientInfo.address);
|
|
190
|
+
}
|
|
191
|
+
fwdSocket.close();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
fwdSocket.on("error", (err) => {
|
|
196
|
+
if (!handled) {
|
|
197
|
+
handled = true;
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
fwdSocket.close();
|
|
200
|
+
const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
|
|
201
|
+
this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
fwdSocket.send(data, 0, data.length, 53, this.fallbackDns);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private handleUdpMessage(data: Buffer, rinfo: RemoteInfo): void {
|
|
209
|
+
if (this.isRateLimited(rinfo.address)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const response = this.server.resolve(data, rinfo.address);
|
|
215
|
+
if (response) {
|
|
216
|
+
if (response.length > 0) {
|
|
217
|
+
this.udpServer?.send(response, 0, response.length, rinfo.port, rinfo.address);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
if (this.fallbackDns) {
|
|
221
|
+
this.forwardUdpQuery(data, rinfo);
|
|
222
|
+
} else {
|
|
223
|
+
const nx = this.server.generateErrorResponse(data, DNS_RCODE.NXDOMAIN);
|
|
224
|
+
this.udpServer?.send(nx, 0, nx.length, rinfo.port, rinfo.address);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private removeTcpConnection(socket: net.Socket): void {
|
|
232
|
+
const entry = this.activeTcpConnections.get(socket);
|
|
233
|
+
if (entry?.timeoutId) clearTimeout(entry.timeoutId);
|
|
234
|
+
this.activeTcpConnections.delete(socket);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private forwardTcpQuery(query: Buffer, clientSocket: net.Socket, peerAddr: string): void {
|
|
238
|
+
if (!this.fallbackDns) return;
|
|
239
|
+
|
|
240
|
+
const fwd = net.createConnection(53, this.fallbackDns, () => {
|
|
241
|
+
const prefixed = Buffer.alloc(2 + query.length);
|
|
242
|
+
prefixed.writeUInt16BE(query.length, 0);
|
|
243
|
+
query.copy(prefixed, 2);
|
|
244
|
+
fwd.write(prefixed);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
fwd.on("data", (data) => {
|
|
248
|
+
if (data.length < 2) return;
|
|
249
|
+
const resp = data.subarray(2);
|
|
250
|
+
|
|
251
|
+
const ips = this.parseResolvedIpv4s(resp);
|
|
252
|
+
let blockedIp = null;
|
|
253
|
+
|
|
254
|
+
for (const ip of ips) {
|
|
255
|
+
if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
|
|
256
|
+
blockedIp = ip;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (blockedIp) {
|
|
262
|
+
if (!clientSocket.destroyed) {
|
|
263
|
+
const ref = this.server.generateErrorResponse(query, DNS_RCODE.REFUSED);
|
|
264
|
+
const p = Buffer.alloc(2 + ref.length);
|
|
265
|
+
p.writeUInt16BE(ref.length, 0);
|
|
266
|
+
ref.copy(p, 2);
|
|
267
|
+
clientSocket.write(p);
|
|
268
|
+
clientSocket.end();
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
if (!clientSocket.destroyed) clientSocket.write(data);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
fwd.on("end", () => {
|
|
276
|
+
if (!clientSocket.destroyed) clientSocket.end();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
fwd.on("error", (err) => {
|
|
280
|
+
if (!clientSocket.destroyed) {
|
|
281
|
+
const sf = this.server.generateErrorResponse(query, DNS_RCODE.SERVFAIL);
|
|
282
|
+
const p = Buffer.alloc(2 + sf.length);
|
|
283
|
+
p.writeUInt16BE(sf.length, 0);
|
|
284
|
+
sf.copy(p, 2);
|
|
285
|
+
clientSocket.write(p);
|
|
286
|
+
clientSocket.end();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private handleTcpConnection(socket: net.Socket): void {
|
|
292
|
+
let buffer = Buffer.alloc(0);
|
|
293
|
+
const peerAddr = socket.remoteAddress || "unknown";
|
|
294
|
+
|
|
295
|
+
socket.on("close", () => this.removeTcpConnection(socket));
|
|
296
|
+
socket.on("error", (err) => {
|
|
297
|
+
this.removeTcpConnection(socket);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (this.isRateLimited(peerAddr)) {
|
|
301
|
+
socket.destroy();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
socket.on("data", (data: Buffer) => {
|
|
306
|
+
if (buffer.length + data.length > MAX_TCP_BUFFER_SIZE) {
|
|
307
|
+
socket.destroy();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const combined = Buffer.concat([buffer, data]);
|
|
312
|
+
buffer = combined;
|
|
313
|
+
|
|
314
|
+
while (buffer.length >= 2) {
|
|
315
|
+
const length = buffer.readUInt16BE(0);
|
|
316
|
+
|
|
317
|
+
if (buffer.length < 2 + length) continue;
|
|
318
|
+
|
|
319
|
+
const query = buffer.subarray(2, 2 + length);
|
|
320
|
+
buffer = buffer.subarray(2 + length);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const response = this.server.resolve(query, peerAddr);
|
|
324
|
+
if (response) {
|
|
325
|
+
if (response.length > 0) {
|
|
326
|
+
const prefixed = Buffer.alloc(2 + response.length);
|
|
327
|
+
prefixed.writeUInt16BE(response.length, 0);
|
|
328
|
+
response.copy(prefixed, 2);
|
|
329
|
+
socket.write(prefixed);
|
|
330
|
+
} else {
|
|
331
|
+
socket.end();
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
if (this.fallbackDns) {
|
|
335
|
+
this.forwardTcpQuery(query, socket, peerAddr);
|
|
336
|
+
} else {
|
|
337
|
+
const nx = this.server.generateErrorResponse(query, DNS_RCODE.NXDOMAIN);
|
|
338
|
+
const p = Buffer.alloc(2 + nx.length);
|
|
339
|
+
p.writeUInt16BE(nx.length, 0);
|
|
340
|
+
nx.copy(p, 2);
|
|
341
|
+
socket.write(p);
|
|
342
|
+
socket.end();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
socket.end();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getPort(): number {
|
|
353
|
+
return this.port;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "assert";
|
|
3
|
+
import { DevDnsServer } from "./dns-service.js";
|
|
4
|
+
import { ServerConfig, DNS_TYPES, DNS_RCODE } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function buildQuery(name: string, type: number): Buffer {
|
|
7
|
+
const encoder = new (class {
|
|
8
|
+
buf = Buffer.alloc(256);
|
|
9
|
+
offset = 0;
|
|
10
|
+
|
|
11
|
+
writeUint16(v: number) {
|
|
12
|
+
this.buf.writeUInt16BE(v, this.offset);
|
|
13
|
+
this.offset += 2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
writeUint8(v: number) {
|
|
17
|
+
this.buf.writeUInt8(v, this.offset);
|
|
18
|
+
this.offset += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
writeDomainName(name: string) {
|
|
22
|
+
for (const label of name.split(".")) {
|
|
23
|
+
if (label.length === 0) continue;
|
|
24
|
+
this.writeUint8(label.length);
|
|
25
|
+
Buffer.from(label).copy(this.buf, this.offset);
|
|
26
|
+
this.offset += label.length;
|
|
27
|
+
}
|
|
28
|
+
this.writeUint8(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
finish(): Buffer {
|
|
32
|
+
return this.buf.subarray(0, this.offset);
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
|
|
36
|
+
encoder.writeUint16(0x1234);
|
|
37
|
+
encoder.writeUint16(0x0100);
|
|
38
|
+
encoder.writeUint16(1);
|
|
39
|
+
encoder.writeUint16(0);
|
|
40
|
+
encoder.writeUint16(0);
|
|
41
|
+
encoder.writeUint16(0);
|
|
42
|
+
|
|
43
|
+
encoder.writeDomainName(name);
|
|
44
|
+
encoder.writeUint16(type);
|
|
45
|
+
encoder.writeUint16(1);
|
|
46
|
+
|
|
47
|
+
return encoder.finish();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildMalformedQuery(): Buffer {
|
|
51
|
+
return Buffer.from([0x12, 0x34]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseResponseFlags(buf: Buffer): { qr: number; rcode: number; id: number } {
|
|
55
|
+
const id = buf.readUInt16BE(0);
|
|
56
|
+
const flags = buf.readUInt16BE(2);
|
|
57
|
+
const qr = (flags >> 15) & 0x1;
|
|
58
|
+
const rcode = flags & 0xf;
|
|
59
|
+
return { id, qr, rcode };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getAnswerCount(buf: Buffer): number {
|
|
63
|
+
return buf.readUInt16BE(6);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("DevDnsServer - Baseline Tests", () => {
|
|
67
|
+
let server: DevDnsServer;
|
|
68
|
+
|
|
69
|
+
it("returns A record with matching ID and NOERROR for configured host", async () => {
|
|
70
|
+
const config: ServerConfig = {
|
|
71
|
+
port: 53,
|
|
72
|
+
hosts: {
|
|
73
|
+
"test.loop": {
|
|
74
|
+
records: [{ type: "A", address: "127.0.0.1" }],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
server = new DevDnsServer(config);
|
|
80
|
+
const queryBuffer = buildQuery("test.loop", DNS_TYPES.A);
|
|
81
|
+
const response = server.resolve(queryBuffer)!;
|
|
82
|
+
|
|
83
|
+
assert.ok(response.length > 0);
|
|
84
|
+
|
|
85
|
+
const { id, qr, rcode } = parseResponseFlags(response);
|
|
86
|
+
assert.strictEqual(id, 0x1234);
|
|
87
|
+
assert.strictEqual(qr, 1);
|
|
88
|
+
assert.strictEqual(rcode, DNS_RCODE.NOERROR);
|
|
89
|
+
|
|
90
|
+
const ancount = getAnswerCount(response);
|
|
91
|
+
assert.ok(ancount >= 1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns NXDOMAIN for unconfigured host", async () => {
|
|
95
|
+
const config: ServerConfig = {
|
|
96
|
+
port: 53,
|
|
97
|
+
hosts: {},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
server = new DevDnsServer(config);
|
|
101
|
+
const queryBuffer = buildQuery("notexist.loop", DNS_TYPES.A);
|
|
102
|
+
const response = server.resolve(queryBuffer)!;
|
|
103
|
+
|
|
104
|
+
assert.ok(response.length > 0);
|
|
105
|
+
|
|
106
|
+
const { id, qr, rcode } = parseResponseFlags(response);
|
|
107
|
+
assert.strictEqual(id, 0x1234);
|
|
108
|
+
assert.strictEqual(qr, 1);
|
|
109
|
+
assert.strictEqual(rcode, DNS_RCODE.NXDOMAIN);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns CNAME record when configured", async () => {
|
|
113
|
+
const config: ServerConfig = {
|
|
114
|
+
port: 53,
|
|
115
|
+
hosts: {
|
|
116
|
+
"alias.loop": {
|
|
117
|
+
records: [{ type: "CNAME", target: "target.loop" }],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
server = new DevDnsServer(config);
|
|
123
|
+
const queryBuffer = buildQuery("alias.loop", DNS_TYPES.CNAME);
|
|
124
|
+
const response = server.resolve(queryBuffer)!;
|
|
125
|
+
|
|
126
|
+
assert.ok(response.length > 0);
|
|
127
|
+
|
|
128
|
+
const { qr, rcode } = parseResponseFlags(response);
|
|
129
|
+
assert.strictEqual(qr, 1);
|
|
130
|
+
assert.strictEqual(rcode, DNS_RCODE.NOERROR);
|
|
131
|
+
|
|
132
|
+
const ancount = getAnswerCount(response);
|
|
133
|
+
assert.ok(ancount >= 1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("returns TXT record when configured", async () => {
|
|
137
|
+
const config: ServerConfig = {
|
|
138
|
+
port: 53,
|
|
139
|
+
hosts: {
|
|
140
|
+
"txt.loop": {
|
|
141
|
+
records: [{ type: "TXT", data: ["v=spf1 include:_example.com ~all"] }],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
server = new DevDnsServer(config);
|
|
147
|
+
const queryBuffer = buildQuery("txt.loop", DNS_TYPES.TXT);
|
|
148
|
+
const response = server.resolve(queryBuffer)!;
|
|
149
|
+
|
|
150
|
+
assert.ok(response.length > 0);
|
|
151
|
+
|
|
152
|
+
const { qr } = parseResponseFlags(response);
|
|
153
|
+
assert.strictEqual(qr, 1);
|
|
154
|
+
|
|
155
|
+
const ancount = getAnswerCount(response);
|
|
156
|
+
assert.ok(ancount >= 1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("rejects malformed (too short) query packets", async () => {
|
|
160
|
+
const config: ServerConfig = {
|
|
161
|
+
port: 53,
|
|
162
|
+
hosts: {
|
|
163
|
+
"test.loop": {
|
|
164
|
+
records: [{ type: "A", address: "127.0.0.1" }],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
server = new DevDnsServer(config);
|
|
170
|
+
const malformed = buildMalformedQuery();
|
|
171
|
+
const response = server.resolve(malformed)!;
|
|
172
|
+
|
|
173
|
+
assert.strictEqual(response.length, 0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("rejects packets that are DNS responses (QR=1)", async () => {
|
|
177
|
+
const config: ServerConfig = {
|
|
178
|
+
port: 53,
|
|
179
|
+
hosts: {
|
|
180
|
+
"test.loop": {
|
|
181
|
+
records: [{ type: "A", address: "127.0.0.1" }],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
server = new DevDnsServer(config);
|
|
187
|
+
|
|
188
|
+
const encoder = new (class {
|
|
189
|
+
buf = Buffer.alloc(256);
|
|
190
|
+
offset = 0;
|
|
191
|
+
|
|
192
|
+
writeUint16(v: number) {
|
|
193
|
+
this.buf.writeUInt16BE(v, this.offset);
|
|
194
|
+
this.offset += 2;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
writeDomainName(name: string) {
|
|
198
|
+
for (const label of name.split(".")) {
|
|
199
|
+
if (label.length === 0) continue;
|
|
200
|
+
this.writeUint8(label.length);
|
|
201
|
+
Buffer.from(label).copy(this.buf, this.offset);
|
|
202
|
+
this.offset += label.length;
|
|
203
|
+
}
|
|
204
|
+
this.writeUint8(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeUint8(v: number) {
|
|
208
|
+
this.buf.writeUInt8(v, this.offset);
|
|
209
|
+
this.offset += 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
finish(): Buffer {
|
|
213
|
+
return this.buf.subarray(0, this.offset);
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
|
|
217
|
+
encoder.writeUint16(0xdead);
|
|
218
|
+
encoder.writeUint16(0x8100);
|
|
219
|
+
encoder.writeUint16(1);
|
|
220
|
+
encoder.writeUint16(0);
|
|
221
|
+
encoder.writeUint16(0);
|
|
222
|
+
encoder.writeUint16(0);
|
|
223
|
+
encoder.writeDomainName("test.loop");
|
|
224
|
+
encoder.writeUint16(DNS_TYPES.A);
|
|
225
|
+
encoder.writeUint16(1);
|
|
226
|
+
|
|
227
|
+
const response = server.resolve(encoder.finish())!;
|
|
228
|
+
assert.strictEqual(response.length, 0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not process queries with RD=0 (recursion not desired)", async () => {
|
|
232
|
+
const config: ServerConfig = {
|
|
233
|
+
port: 53,
|
|
234
|
+
hosts: {
|
|
235
|
+
"test.loop": {
|
|
236
|
+
records: [{ type: "A", address: "127.0.0.1" }],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
server = new DevDnsServer(config);
|
|
242
|
+
|
|
243
|
+
const encoder = new (class {
|
|
244
|
+
buf = Buffer.alloc(256);
|
|
245
|
+
offset = 0;
|
|
246
|
+
|
|
247
|
+
writeUint16(v: number) {
|
|
248
|
+
this.buf.writeUInt16BE(v, this.offset);
|
|
249
|
+
this.offset += 2;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
writeDomainName(name: string) {
|
|
253
|
+
for (const label of name.split(".")) {
|
|
254
|
+
if (label.length === 0) continue;
|
|
255
|
+
this.writeUint8(label.length);
|
|
256
|
+
Buffer.from(label).copy(this.buf, this.offset);
|
|
257
|
+
this.offset += label.length;
|
|
258
|
+
}
|
|
259
|
+
this.writeUint8(0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
writeUint8(v: number) {
|
|
263
|
+
this.buf.writeUInt8(v, this.offset);
|
|
264
|
+
this.offset += 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
finish(): Buffer {
|
|
268
|
+
return this.buf.subarray(0, this.offset);
|
|
269
|
+
}
|
|
270
|
+
})();
|
|
271
|
+
|
|
272
|
+
encoder.writeUint16(0xbeef);
|
|
273
|
+
encoder.writeUint16(0x0000);
|
|
274
|
+
encoder.writeUint16(1);
|
|
275
|
+
encoder.writeUint16(0);
|
|
276
|
+
encoder.writeUint16(0);
|
|
277
|
+
encoder.writeUint16(0);
|
|
278
|
+
encoder.writeDomainName("test.loop");
|
|
279
|
+
encoder.writeUint16(DNS_TYPES.A);
|
|
280
|
+
encoder.writeUint16(1);
|
|
281
|
+
|
|
282
|
+
const response = server.resolve(encoder.finish())!;
|
|
283
|
+
assert.strictEqual(response.length, 0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("handles multiple host lookups correctly (isolated hosts)", async () => {
|
|
287
|
+
const config: ServerConfig = {
|
|
288
|
+
port: 53,
|
|
289
|
+
hosts: {
|
|
290
|
+
"alpha.loop": {
|
|
291
|
+
records: [{ type: "A", address: "10.0.0.1" }],
|
|
292
|
+
},
|
|
293
|
+
"beta.loop": {
|
|
294
|
+
records: [{ type: "A", address: "10.0.0.2" }],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
server = new DevDnsServer(config);
|
|
300
|
+
|
|
301
|
+
const alphaResponse = server.resolve(buildQuery("alpha.loop", DNS_TYPES.A))!;
|
|
302
|
+
assert.ok(alphaResponse.length > 0);
|
|
303
|
+
const { qr: qrAlpha } = parseResponseFlags(alphaResponse);
|
|
304
|
+
assert.strictEqual(qrAlpha, 1);
|
|
305
|
+
|
|
306
|
+
const betaResponse = server.resolve(buildQuery("beta.loop", DNS_TYPES.A))!;
|
|
307
|
+
assert.ok(betaResponse.length > 0);
|
|
308
|
+
const { qr: qrBeta } = parseResponseFlags(betaResponse);
|
|
309
|
+
assert.strictEqual(qrBeta, 1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("supports AAAA records", async () => {
|
|
313
|
+
const config: ServerConfig = {
|
|
314
|
+
port: 53,
|
|
315
|
+
hosts: {
|
|
316
|
+
"ipv6.loop": {
|
|
317
|
+
records: [{ type: "AAAA", address: "::1" }],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
server = new DevDnsServer(config);
|
|
323
|
+
const queryBuffer = buildQuery("ipv6.loop", DNS_TYPES.AAAA);
|
|
324
|
+
const response = server.resolve(queryBuffer)!;
|
|
325
|
+
|
|
326
|
+
assert.ok(response.length > 0);
|
|
327
|
+
|
|
328
|
+
const { qr } = parseResponseFlags(response);
|
|
329
|
+
assert.strictEqual(qr, 1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("supports MX records", async () => {
|
|
333
|
+
const config: ServerConfig = {
|
|
334
|
+
port: 53,
|
|
335
|
+
hosts: {
|
|
336
|
+
"mx.loop": {
|
|
337
|
+
records: [{ type: "MX", priority: 10, exchange: "mail.mx.loop" }],
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
server = new DevDnsServer(config);
|
|
343
|
+
const queryBuffer = buildQuery("mx.loop", DNS_TYPES.MX);
|
|
344
|
+
const response = server.resolve(queryBuffer)!;
|
|
345
|
+
|
|
346
|
+
assert.ok(response.length > 0);
|
|
347
|
+
|
|
348
|
+
const { qr } = parseResponseFlags(response);
|
|
349
|
+
assert.strictEqual(qr, 1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("supports SRV records", async () => {
|
|
353
|
+
const config: ServerConfig = {
|
|
354
|
+
port: 53,
|
|
355
|
+
hosts: {
|
|
356
|
+
"srv.loop": {
|
|
357
|
+
records: [{ type: "SRV", priority: 10, weight: 5, port: 8080, target: "app.srv.loop" }],
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
server = new DevDnsServer(config);
|
|
363
|
+
const queryBuffer = buildQuery("srv.loop", DNS_TYPES.SRV);
|
|
364
|
+
const response = server.resolve(queryBuffer)!;
|
|
365
|
+
|
|
366
|
+
assert.ok(response.length > 0);
|
|
367
|
+
|
|
368
|
+
const { qr } = parseResponseFlags(response);
|
|
369
|
+
assert.strictEqual(qr, 1);
|
|
370
|
+
});
|
|
371
|
+
});
|