@opensecurity/zonzon-core 0.1.4 → 0.1.5

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/dist/audit.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export declare class AuditLogger {
2
2
  private isTestEnv;
3
+ private useJson;
4
+ private metrics;
3
5
  private sanitize;
6
+ setJsonMode(enable: boolean): void;
7
+ getMetricsPrometheus(): string;
4
8
  dns(ip: string, questions: any[], rcode: number, cached?: boolean): void;
5
9
  firewall(ip: string, target: string, action: "ALLOW" | "DENY", detail?: string): void;
6
10
  http(ip: string, method: string, host: string, path: string, status: number, target?: string): void;
package/dist/audit.js CHANGED
@@ -2,38 +2,144 @@ import { DNS_TYPES } from "./types.js";
2
2
  const REVERSE_DNS_TYPES = Object.fromEntries(Object.entries(DNS_TYPES).map(([k, v]) => [v, k]));
3
3
  export class AuditLogger {
4
4
  isTestEnv = process.argv.includes("--test") || process.env.NODE_ENV === "test";
5
+ useJson = false;
6
+ metrics = {
7
+ dns_queries: 0,
8
+ dns_blocked: 0,
9
+ http_requests: 0,
10
+ http_blocked: 0,
11
+ firewall_drops: 0,
12
+ system_events: 0,
13
+ errors: 0
14
+ };
5
15
  sanitize(input) {
6
16
  return String(input || "").replace(/[\r\n\t]/g, " ").replace(/[^\x20-\x7E]/g, "?");
7
17
  }
18
+ setJsonMode(enable) {
19
+ this.useJson = enable;
20
+ }
21
+ getMetricsPrometheus() {
22
+ return [
23
+ `# HELP zonzon_dns_queries_total Total DNS queries processed`,
24
+ `# TYPE zonzon_dns_queries_total counter`,
25
+ `zonzon_dns_queries_total ${this.metrics.dns_queries}`,
26
+ `# HELP zonzon_dns_blocked_total Total DNS queries blocked by policy`,
27
+ `# TYPE zonzon_dns_blocked_total counter`,
28
+ `zonzon_dns_blocked_total ${this.metrics.dns_blocked}`,
29
+ `# HELP zonzon_http_requests_total Total HTTP requests routed`,
30
+ `# TYPE zonzon_http_requests_total counter`,
31
+ `zonzon_http_requests_total ${this.metrics.http_requests}`,
32
+ `# HELP zonzon_http_blocked_total Total HTTP requests blocked`,
33
+ `# TYPE zonzon_http_blocked_total counter`,
34
+ `zonzon_http_blocked_total ${this.metrics.http_blocked}`,
35
+ `# HELP zonzon_firewall_drops_total Total connection drops across L3/L4/L7`,
36
+ `# TYPE zonzon_firewall_drops_total counter`,
37
+ `zonzon_firewall_drops_total ${this.metrics.firewall_drops}`,
38
+ `# HELP zonzon_errors_total Total system errors encountered`,
39
+ `# TYPE zonzon_errors_total counter`,
40
+ `zonzon_errors_total ${this.metrics.errors}`
41
+ ].join("\n") + "\n";
42
+ }
8
43
  dns(ip, questions, rcode, cached = false) {
44
+ this.metrics.dns_queries += questions.length;
45
+ if (rcode === 5 || rcode === 3)
46
+ this.metrics.dns_blocked += questions.length;
9
47
  if (this.isTestEnv)
10
48
  return;
11
- const codeMap = { 0: "Found (NOERROR)", 3: "Not Found (NXDOMAIN)", 5: "Blocked by Firewall (REFUSED)" };
12
- const prefix = cached ? "[Cached] " : "";
13
- questions.forEach(q => {
14
- console.log(`[DNS] ${this.sanitize(ip)} | ${prefix}${REVERSE_DNS_TYPES[q.type] || q.type} ${this.sanitize(q.name)} -> ${codeMap[rcode] || rcode}`);
15
- });
49
+ if (this.useJson) {
50
+ console.log(JSON.stringify({
51
+ timestamp: new Date().toISOString(),
52
+ level: "INFO",
53
+ component: "DNS",
54
+ ip,
55
+ questions: questions.map(q => ({ name: q.name, type: REVERSE_DNS_TYPES[q.type] || q.type })),
56
+ rcode,
57
+ cached
58
+ }));
59
+ }
60
+ else {
61
+ const codeMap = { 0: "Found (NOERROR)", 3: "Not Found (NXDOMAIN)", 5: "Blocked by Firewall (REFUSED)" };
62
+ const prefix = cached ? "[Cached] " : "";
63
+ questions.forEach(q => {
64
+ console.log(`[DNS] ${this.sanitize(ip)} | ${prefix}${REVERSE_DNS_TYPES[q.type] || q.type} ${this.sanitize(q.name)} -> ${codeMap[rcode] || rcode}`);
65
+ });
66
+ }
16
67
  }
17
68
  firewall(ip, target, action, detail = "") {
69
+ if (action === "DENY")
70
+ this.metrics.firewall_drops++;
18
71
  if (this.isTestEnv)
19
72
  return;
20
- const color = action === "ALLOW" ? "\x1b[32mALLOW\x1b[0m" : "\x1b[31mDENY\x1b[0m";
21
- console.log(`[FIREWALL] ${this.sanitize(ip)} | ${color} | ${this.sanitize(target)} ${detail ? `(${this.sanitize(detail)})` : ""}`);
73
+ if (this.useJson) {
74
+ console.log(JSON.stringify({
75
+ timestamp: new Date().toISOString(),
76
+ level: action === "DENY" ? "WARN" : "INFO",
77
+ component: "FIREWALL",
78
+ action,
79
+ ip,
80
+ target,
81
+ detail
82
+ }));
83
+ }
84
+ else {
85
+ const color = action === "ALLOW" ? "\x1b[32mALLOW\x1b[0m" : "\x1b[31mDENY\x1b[0m";
86
+ console.log(`[FIREWALL] ${this.sanitize(ip)} | ${color} | ${this.sanitize(target)} ${detail ? `(${this.sanitize(detail)})` : ""}`);
87
+ }
22
88
  }
23
89
  http(ip, method, host, path, status, target = "") {
90
+ this.metrics.http_requests++;
91
+ if (status === 403 || status === 413 || status === 429)
92
+ this.metrics.http_blocked++;
24
93
  if (this.isTestEnv)
25
94
  return;
26
- console.log(`[HTTP] ${this.sanitize(ip)} | Returned Status ${status} | ${this.sanitize(method)} ${this.sanitize(host)}${this.sanitize(path)} ${target ? `-> ${this.sanitize(target)}` : ""}`);
95
+ if (this.useJson) {
96
+ console.log(JSON.stringify({
97
+ timestamp: new Date().toISOString(),
98
+ level: status >= 400 ? "WARN" : "INFO",
99
+ component: "HTTP",
100
+ ip,
101
+ method,
102
+ host,
103
+ path,
104
+ status,
105
+ target
106
+ }));
107
+ }
108
+ else {
109
+ console.log(`[HTTP] ${this.sanitize(ip)} | Returned Status ${status} | ${this.sanitize(method)} ${this.sanitize(host)}${this.sanitize(path)} ${target ? `-> ${this.sanitize(target)}` : ""}`);
110
+ }
27
111
  }
28
112
  system(msg) {
113
+ this.metrics.system_events++;
29
114
  if (this.isTestEnv)
30
115
  return;
31
- console.log(`[SYSTEM] ${this.sanitize(msg)}`);
116
+ if (this.useJson) {
117
+ console.log(JSON.stringify({
118
+ timestamp: new Date().toISOString(),
119
+ level: "INFO",
120
+ component: "SYSTEM",
121
+ message: msg
122
+ }));
123
+ }
124
+ else {
125
+ console.log(`[SYSTEM] ${this.sanitize(msg)}`);
126
+ }
32
127
  }
33
128
  error(msg) {
129
+ this.metrics.errors++;
34
130
  if (this.isTestEnv)
35
131
  return;
36
- console.error(`[ERROR] \x1b[31m${this.sanitize(msg)}\x1b[0m`);
132
+ if (this.useJson) {
133
+ console.error(JSON.stringify({
134
+ timestamp: new Date().toISOString(),
135
+ level: "ERROR",
136
+ component: "SYSTEM",
137
+ message: msg
138
+ }));
139
+ }
140
+ else {
141
+ console.error(`[ERROR] \x1b[31m${this.sanitize(msg)}\x1b[0m`);
142
+ }
37
143
  }
38
144
  }
39
145
  export const audit = new AuditLogger();
@@ -4,20 +4,29 @@ export declare class DnsHandler {
4
4
  private server;
5
5
  private udpServer;
6
6
  private tcpServer;
7
+ private dotServer;
8
+ private dohServer;
7
9
  private port;
8
10
  private fallbackDns;
9
11
  private config;
10
12
  private activeTcpConnections;
11
13
  private maxTcpConnections;
12
14
  private tcpIdleTimeoutMs;
15
+ private pendingUpstreamQueries;
16
+ private readonly maxConcurrentUdpForwards;
13
17
  private rateLimiter;
14
18
  constructor(server: DevDnsServer, config: ServerConfig);
15
19
  start(): Promise<void>;
16
20
  stop(): Promise<void>;
17
21
  private startUdp;
18
22
  private startTcp;
23
+ private startDoT;
24
+ private startDoH;
19
25
  private isRateLimited;
20
26
  private parseResolvedIpv4s;
27
+ private isPrivateIp;
28
+ private resolveQueryAsync;
29
+ private handleDohRequest;
21
30
  private forwardUdpQuery;
22
31
  private handleUdpMessage;
23
32
  private removeTcpConnection;
@@ -1,20 +1,28 @@
1
1
  import dgram from "dgram";
2
2
  import * as net from "net";
3
- import { DnsWireFormat } from "./dns-service.js";
3
+ import * as tls from "node:tls";
4
+ import * as https from "node:https";
5
+ import { randomBytes } from "node:crypto";
6
+ import { DnsWireFormat, extractQuestions } from "./dns-service.js";
4
7
  import { DNS_RCODE } from "./types.js";
5
8
  import { RateLimiter } from "./rate-limiter.js";
9
+ import { audit } from "./audit.js";
6
10
  import { firewallEngine } from "./firewall.js";
7
11
  const MAX_TCP_BUFFER_SIZE = 64 * 1024;
8
12
  export class DnsHandler {
9
13
  server;
10
14
  udpServer = null;
11
15
  tcpServer = null;
16
+ dotServer = null;
17
+ dohServer = null;
12
18
  port;
13
19
  fallbackDns;
14
20
  config;
15
21
  activeTcpConnections = new Map();
16
22
  maxTcpConnections;
17
23
  tcpIdleTimeoutMs;
24
+ pendingUpstreamQueries = new Map();
25
+ maxConcurrentUdpForwards = 2000;
18
26
  rateLimiter;
19
27
  constructor(server, config) {
20
28
  this.server = server;
@@ -36,6 +44,10 @@ export class DnsHandler {
36
44
  async start() {
37
45
  await this.startUdp();
38
46
  await this.startTcp();
47
+ if (this.config.tls) {
48
+ await this.startDoT();
49
+ await this.startDoH();
50
+ }
39
51
  }
40
52
  async stop() {
41
53
  if (this.udpServer) {
@@ -44,12 +56,32 @@ export class DnsHandler {
44
56
  });
45
57
  this.udpServer = null;
46
58
  }
59
+ for (const pending of this.pendingUpstreamQueries.values()) {
60
+ clearTimeout(pending.timeoutId);
61
+ }
62
+ this.pendingUpstreamQueries.clear();
47
63
  if (this.tcpServer) {
48
64
  await new Promise((resolve) => {
49
65
  this.tcpServer?.close(() => resolve());
50
66
  });
51
67
  this.tcpServer = null;
52
68
  }
69
+ if (this.dotServer) {
70
+ await new Promise((resolve) => {
71
+ this.dotServer?.close(() => resolve());
72
+ });
73
+ this.dotServer = null;
74
+ }
75
+ if (this.dohServer) {
76
+ if ('closeIdleConnections' in this.dohServer) {
77
+ this.dohServer.closeIdleConnections();
78
+ }
79
+ await new Promise((resolve) => {
80
+ this.dohServer?.close(() => resolve());
81
+ });
82
+ this.dohServer = null;
83
+ }
84
+ this.activeTcpConnections.clear();
53
85
  }
54
86
  startUdp() {
55
87
  return new Promise((resolve, reject) => {
@@ -88,13 +120,6 @@ export class DnsHandler {
88
120
  });
89
121
  this.handleTcpConnection(socket);
90
122
  });
91
- tcpServer.on("close", () => {
92
- for (const [socket] of this.activeTcpConnections) {
93
- if (!socket.destroyed)
94
- socket.destroy();
95
- }
96
- this.activeTcpConnections.clear();
97
- });
98
123
  tcpServer.on("listening", () => {
99
124
  this.tcpServer = tcpServer;
100
125
  resolve();
@@ -102,6 +127,63 @@ export class DnsHandler {
102
127
  tcpServer.listen(this.port, "0.0.0.0");
103
128
  });
104
129
  }
130
+ startDoT() {
131
+ return new Promise((resolve) => {
132
+ if (!this.config.tls) {
133
+ resolve();
134
+ return;
135
+ }
136
+ const dotServer = tls.createServer({
137
+ cert: this.config.tls.cert,
138
+ key: this.config.tls.key,
139
+ minVersion: "TLSv1.2"
140
+ }, (socket) => {
141
+ if (this.activeTcpConnections.size >= this.maxTcpConnections) {
142
+ socket.destroy();
143
+ return;
144
+ }
145
+ this.activeTcpConnections.set(socket, { timeoutId: undefined });
146
+ socket.setTimeout(this.tcpIdleTimeoutMs);
147
+ socket.on("timeout", () => {
148
+ this.removeTcpConnection(socket);
149
+ socket.destroy();
150
+ });
151
+ this.handleTcpConnection(socket);
152
+ });
153
+ dotServer.on("listening", () => {
154
+ this.dotServer = dotServer;
155
+ audit.system(`DoT (DNS over TLS) isolated boundary listening on port ${this.config.dotPort || 853}`);
156
+ resolve();
157
+ });
158
+ dotServer.listen(this.config.dotPort || 853, "0.0.0.0");
159
+ });
160
+ }
161
+ startDoH() {
162
+ return new Promise((resolve) => {
163
+ if (!this.config.tls) {
164
+ resolve();
165
+ return;
166
+ }
167
+ const dohServer = https.createServer({
168
+ cert: this.config.tls.cert,
169
+ key: this.config.tls.key,
170
+ minVersion: "TLSv1.2"
171
+ }, (req, res) => {
172
+ this.handleDohRequest(req, res).catch(() => {
173
+ if (!res.headersSent) {
174
+ res.writeHead(500);
175
+ res.end();
176
+ }
177
+ });
178
+ });
179
+ dohServer.on("listening", () => {
180
+ this.dohServer = dohServer;
181
+ audit.system(`DoH (DNS over HTTPS) isolated boundary listening on port ${this.config.dohPort || 8443}`);
182
+ resolve();
183
+ });
184
+ dohServer.listen(this.config.dohPort || 8443, "0.0.0.0");
185
+ });
186
+ }
105
187
  isRateLimited(ip) {
106
188
  if (!this.rateLimiter)
107
189
  return false;
@@ -133,24 +215,53 @@ export class DnsHandler {
133
215
  catch { }
134
216
  return ips;
135
217
  }
136
- forwardUdpQuery(data, clientInfo) {
137
- if (!this.fallbackDns || !this.udpServer)
138
- return;
139
- const fwdSocket = dgram.createSocket("udp4");
140
- let handled = false;
141
- const timeout = setTimeout(() => {
142
- if (!handled) {
143
- handled = true;
144
- fwdSocket.close();
145
- const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
146
- this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
218
+ isPrivateIp(ip) {
219
+ if (!net.isIPv4(ip) && !net.isIPv6(ip))
220
+ return false;
221
+ if (net.isIPv6(ip)) {
222
+ const normalized = ip.toLowerCase();
223
+ return normalized === "::1" || normalized.startsWith("fe80:") || normalized.startsWith("fc00:") || normalized.startsWith("fd");
224
+ }
225
+ const parts = ip.split('.').map(Number);
226
+ if (parts[0] === 10)
227
+ return true;
228
+ if (parts[0] === 127)
229
+ return true;
230
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
231
+ return true;
232
+ if (parts[0] === 192 && parts[1] === 168)
233
+ return true;
234
+ return false;
235
+ }
236
+ resolveQueryAsync(query, clientIp) {
237
+ return new Promise((resolve) => {
238
+ const response = this.server.resolve(query, clientIp);
239
+ if (response) {
240
+ if (response.length > 0)
241
+ resolve(response);
242
+ else
243
+ resolve(this.server.generateErrorResponse(query, DNS_RCODE.SERVFAIL));
244
+ return;
147
245
  }
148
- }, 3000);
149
- fwdSocket.on("message", (resp) => {
150
- if (!handled) {
151
- handled = true;
152
- clearTimeout(timeout);
153
- const ips = this.parseResolvedIpv4s(resp);
246
+ if (!this.fallbackDns) {
247
+ resolve(this.server.generateErrorResponse(query, DNS_RCODE.NXDOMAIN));
248
+ return;
249
+ }
250
+ const fwdSocket = dgram.createSocket("udp4");
251
+ const timeoutId = setTimeout(() => {
252
+ try {
253
+ fwdSocket.close();
254
+ }
255
+ catch { }
256
+ resolve(this.server.generateErrorResponse(query, DNS_RCODE.SERVFAIL));
257
+ }, 3000);
258
+ fwdSocket.on("message", (msg) => {
259
+ clearTimeout(timeoutId);
260
+ try {
261
+ fwdSocket.close();
262
+ }
263
+ catch { }
264
+ const ips = this.parseResolvedIpv4s(msg);
154
265
  let blockedIp = null;
155
266
  for (const ip of ips) {
156
267
  if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
@@ -158,26 +269,178 @@ export class DnsHandler {
158
269
  break;
159
270
  }
160
271
  }
272
+ const questions = extractQuestions(query);
161
273
  if (blockedIp) {
162
- const ref = this.server.generateErrorResponse(data, DNS_RCODE.REFUSED);
163
- this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
274
+ audit.firewall(clientIp, blockedIp, "DENY", "Upstream target IP blocked");
275
+ audit.dns(clientIp, questions, DNS_RCODE.REFUSED, false);
276
+ resolve(this.server.generateErrorResponse(query, DNS_RCODE.REFUSED));
164
277
  }
165
278
  else {
166
- this.udpServer?.send(resp, 0, resp.length, clientInfo.port, clientInfo.address);
279
+ const rcode = msg.length >= 4 ? msg.readUInt16BE(2) & 0xf : 0;
280
+ audit.dns(clientIp, questions, rcode, false);
281
+ resolve(msg);
167
282
  }
283
+ });
284
+ fwdSocket.on("error", () => {
285
+ clearTimeout(timeoutId);
286
+ try {
287
+ fwdSocket.close();
288
+ }
289
+ catch { }
290
+ resolve(this.server.generateErrorResponse(query, DNS_RCODE.SERVFAIL));
291
+ });
292
+ fwdSocket.send(query, 0, query.length, 53, this.fallbackDns);
293
+ });
294
+ }
295
+ async handleDohRequest(req, res) {
296
+ const clientIp = req.socket.remoteAddress || "unknown";
297
+ if (this.isRateLimited(clientIp)) {
298
+ res.writeHead(429);
299
+ res.end();
300
+ return;
301
+ }
302
+ const method = req.method || "GET";
303
+ const url = new URL(req.url || "/", `https://${req.headers.host || "localhost"}`);
304
+ if (url.pathname !== "/dns-query") {
305
+ res.writeHead(404);
306
+ res.end();
307
+ return;
308
+ }
309
+ let query = null;
310
+ if (method === "GET") {
311
+ const dnsParam = url.searchParams.get("dns");
312
+ if (!dnsParam) {
313
+ res.writeHead(400);
314
+ res.end();
315
+ return;
316
+ }
317
+ try {
318
+ query = Buffer.from(dnsParam, "base64url");
319
+ }
320
+ catch {
321
+ res.writeHead(400);
322
+ res.end();
323
+ return;
324
+ }
325
+ }
326
+ else if (method === "POST") {
327
+ if (req.headers["content-type"] !== "application/dns-message") {
328
+ res.writeHead(415);
329
+ res.end();
330
+ return;
331
+ }
332
+ const chunks = [];
333
+ let total = 0;
334
+ for await (const chunk of req) {
335
+ total += chunk.length;
336
+ if (total > MAX_TCP_BUFFER_SIZE) {
337
+ res.writeHead(413);
338
+ res.end();
339
+ return;
340
+ }
341
+ chunks.push(chunk);
342
+ }
343
+ query = Buffer.concat(chunks);
344
+ }
345
+ else {
346
+ res.writeHead(405);
347
+ res.end();
348
+ return;
349
+ }
350
+ if (!query || query.length < 12) {
351
+ res.writeHead(400);
352
+ res.end();
353
+ return;
354
+ }
355
+ try {
356
+ const response = await this.resolveQueryAsync(query, clientIp);
357
+ res.writeHead(200, {
358
+ "Content-Type": "application/dns-message",
359
+ "Content-Length": response.length,
360
+ "Cache-Control": "max-age=0"
361
+ });
362
+ res.end(response);
363
+ }
364
+ catch {
365
+ res.writeHead(502);
366
+ res.end();
367
+ }
368
+ }
369
+ forwardUdpQuery(data, clientInfo) {
370
+ if (!this.fallbackDns)
371
+ return;
372
+ if (!this.isPrivateIp(clientInfo.address)) {
373
+ if (this.config.firewall && (!this.config.firewall.allowlist_ips || !this.config.firewall.allowlist_ips.includes(clientInfo.address))) {
374
+ audit.system(`Dropped UDP forward request from untrusted WAN IP: ${clientInfo.address} (Anti-Amplification)`);
375
+ return;
376
+ }
377
+ }
378
+ if (this.pendingUpstreamQueries.size >= this.maxConcurrentUdpForwards) {
379
+ audit.system("Dropped UDP forward request: Concurrent connection limit reached");
380
+ return;
381
+ }
382
+ if (data.length < 2)
383
+ return;
384
+ const originalId = data.readUInt16BE(0);
385
+ const ephemeralId = randomBytes(2).readUInt16BE(0);
386
+ const trackingId = randomBytes(4).readUInt32BE(0);
387
+ const fwdSocket = dgram.createSocket("udp4");
388
+ const timeoutId = setTimeout(() => {
389
+ this.pendingUpstreamQueries.delete(trackingId);
390
+ try {
168
391
  fwdSocket.close();
169
392
  }
170
- });
171
- fwdSocket.on("error", (err) => {
172
- if (!handled) {
173
- handled = true;
174
- clearTimeout(timeout);
393
+ catch { }
394
+ const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
395
+ this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
396
+ }, 3000);
397
+ this.pendingUpstreamQueries.set(trackingId, { rinfo: clientInfo, originalId, timeoutId });
398
+ fwdSocket.on("message", (msg) => {
399
+ this.pendingUpstreamQueries.delete(trackingId);
400
+ clearTimeout(timeoutId);
401
+ try {
175
402
  fwdSocket.close();
176
- const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
403
+ }
404
+ catch { }
405
+ if (msg.length < 2)
406
+ return;
407
+ const responseId = msg.readUInt16BE(0);
408
+ if (responseId !== ephemeralId)
409
+ return;
410
+ const restoredMsg = Buffer.from(msg);
411
+ restoredMsg.writeUInt16BE(originalId, 0);
412
+ const ips = this.parseResolvedIpv4s(restoredMsg);
413
+ let blockedIp = null;
414
+ for (const ip of ips) {
415
+ if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
416
+ blockedIp = ip;
417
+ break;
418
+ }
419
+ }
420
+ const questions = extractQuestions(restoredMsg);
421
+ if (blockedIp) {
422
+ audit.firewall(clientInfo.address, blockedIp, "DENY", "Upstream target IP blocked");
423
+ audit.dns(clientInfo.address, questions, DNS_RCODE.REFUSED, false);
424
+ const ref = this.server.generateErrorResponse(restoredMsg, DNS_RCODE.REFUSED);
177
425
  this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
178
426
  }
427
+ else {
428
+ const rcode = restoredMsg.length >= 4 ? restoredMsg.readUInt16BE(2) & 0xf : 0;
429
+ audit.dns(clientInfo.address, questions, rcode, false);
430
+ this.udpServer?.send(restoredMsg, 0, restoredMsg.length, clientInfo.port, clientInfo.address);
431
+ }
179
432
  });
180
- fwdSocket.send(data, 0, data.length, 53, this.fallbackDns);
433
+ fwdSocket.on("error", () => {
434
+ this.pendingUpstreamQueries.delete(trackingId);
435
+ clearTimeout(timeoutId);
436
+ try {
437
+ fwdSocket.close();
438
+ }
439
+ catch { }
440
+ });
441
+ const queryToForward = Buffer.from(data);
442
+ queryToForward.writeUInt16BE(ephemeralId, 0);
443
+ fwdSocket.send(queryToForward, 0, queryToForward.length, 53, this.fallbackDns);
181
444
  }
182
445
  handleUdpMessage(data, rinfo) {
183
446
  if (this.isRateLimited(rinfo.address)) {
@@ -218,6 +481,10 @@ export class DnsHandler {
218
481
  query.copy(prefixed, 2);
219
482
  fwd.write(prefixed);
220
483
  });
484
+ fwd.setTimeout(this.tcpIdleTimeoutMs);
485
+ fwd.on("timeout", () => {
486
+ fwd.destroy();
487
+ });
221
488
  fwd.on("data", (data) => {
222
489
  if (data.length < 2)
223
490
  return;
@@ -230,7 +497,10 @@ export class DnsHandler {
230
497
  break;
231
498
  }
232
499
  }
500
+ const questions = extractQuestions(query);
233
501
  if (blockedIp) {
502
+ audit.firewall(peerAddr, blockedIp, "DENY", "Upstream target IP blocked");
503
+ audit.dns(peerAddr, questions, DNS_RCODE.REFUSED, false);
234
504
  if (!clientSocket.destroyed) {
235
505
  const ref = this.server.generateErrorResponse(query, DNS_RCODE.REFUSED);
236
506
  const p = Buffer.alloc(2 + ref.length);
@@ -241,6 +511,8 @@ export class DnsHandler {
241
511
  }
242
512
  }
243
513
  else {
514
+ const rcode = resp.length >= 4 ? resp.readUInt16BE(2) & 0xf : 0;
515
+ audit.dns(peerAddr, questions, rcode, false);
244
516
  if (!clientSocket.destroyed)
245
517
  clientSocket.write(data);
246
518
  }
@@ -23,7 +23,6 @@ export declare function extractQuestions(query: Buffer): ParsedQuestion[];
23
23
  export declare class DevDnsServer {
24
24
  private config;
25
25
  private cacheMap;
26
- private cacheOrder;
27
26
  private dnsCacheMaxSize;
28
27
  private dnsCacheTtlMs;
29
28
  constructor(config: ServerConfig);