@opensecurity/zonzon-core 0.1.1 → 0.1.3

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.
Files changed (71) hide show
  1. package/dist/audit.d.ts +10 -0
  2. package/dist/audit.js +39 -0
  3. package/dist/cache-layer.test.d.ts +1 -0
  4. package/dist/cache-layer.test.js +205 -0
  5. package/dist/cache-multi-question.test.d.ts +1 -0
  6. package/dist/cache-multi-question.test.js +187 -0
  7. package/dist/dns-handler.d.ts +27 -0
  8. package/dist/dns-handler.js +323 -0
  9. package/dist/dns-service.d.ts +45 -0
  10. package/dist/dns-service.js +546 -0
  11. package/dist/dns-service.test.d.ts +1 -0
  12. package/dist/dns-service.test.js +306 -0
  13. package/dist/dns-wireformat.test.d.ts +1 -0
  14. package/dist/dns-wireformat.test.js +669 -0
  15. package/dist/firewall.d.ts +9 -0
  16. package/dist/firewall.js +62 -0
  17. package/dist/http-body-forwarding-integration.test.d.ts +1 -0
  18. package/dist/http-body-forwarding-integration.test.js +318 -0
  19. package/dist/http-body-forwarding.test.d.ts +1 -0
  20. package/dist/http-body-forwarding.test.js +84 -0
  21. package/dist/http-handler.d.ts +21 -0
  22. package/dist/http-handler.js +429 -0
  23. package/dist/http-proxy.d.ts +14 -0
  24. package/dist/http-proxy.js +135 -0
  25. package/dist/http-proxy.test.d.ts +1 -0
  26. package/dist/http-proxy.test.js +375 -0
  27. package/{src/index.ts → dist/index.d.ts} +1 -1
  28. package/dist/index.js +10 -0
  29. package/dist/rate-limiter.d.ts +11 -0
  30. package/dist/rate-limiter.js +33 -0
  31. package/dist/rate-limiter.test.d.ts +1 -0
  32. package/dist/rate-limiter.test.js +149 -0
  33. package/dist/schema.d.ts +12 -0
  34. package/dist/schema.js +124 -0
  35. package/dist/schema.test.d.ts +1 -0
  36. package/dist/schema.test.js +586 -0
  37. package/dist/sni-proxy.d.ts +12 -0
  38. package/dist/sni-proxy.js +141 -0
  39. package/dist/srv-record.test.d.ts +1 -0
  40. package/dist/srv-record.test.js +186 -0
  41. package/dist/tcp-connection-limit.test.d.ts +1 -0
  42. package/dist/tcp-connection-limit.test.js +89 -0
  43. package/dist/types.d.ts +145 -0
  44. package/dist/types.js +34 -0
  45. package/dist/wildcard-matching.test.d.ts +1 -0
  46. package/dist/wildcard-matching.test.js +162 -0
  47. package/package.json +4 -1
  48. package/src/audit.ts +0 -43
  49. package/src/cache-layer.test.ts +0 -236
  50. package/src/cache-multi-question.test.ts +0 -263
  51. package/src/dns-handler.ts +0 -355
  52. package/src/dns-service.test.ts +0 -371
  53. package/src/dns-service.ts +0 -655
  54. package/src/dns-wireformat.test.ts +0 -771
  55. package/src/env.d.ts +0 -1
  56. package/src/firewall.ts +0 -66
  57. package/src/http-body-forwarding-integration.test.ts +0 -357
  58. package/src/http-body-forwarding.test.ts +0 -101
  59. package/src/http-handler.ts +0 -489
  60. package/src/http-proxy.test.ts +0 -440
  61. package/src/http-proxy.ts +0 -148
  62. package/src/rate-limiter.test.ts +0 -144
  63. package/src/rate-limiter.ts +0 -50
  64. package/src/schema.test.ts +0 -685
  65. package/src/schema.ts +0 -137
  66. package/src/sni-proxy.ts +0 -164
  67. package/src/srv-record.test.ts +0 -211
  68. package/src/tcp-connection-limit.test.ts +0 -110
  69. package/src/types.ts +0 -168
  70. package/src/wildcard-matching.test.ts +0 -196
  71. package/tsconfig.json +0 -9
@@ -0,0 +1,323 @@
1
+ import dgram from "dgram";
2
+ import * as net from "net";
3
+ import { DnsWireFormat } from "./dns-service.js";
4
+ import { DNS_RCODE } from "./types.js";
5
+ import { RateLimiter } from "./rate-limiter.js";
6
+ import { firewallEngine } from "./firewall.js";
7
+ const MAX_TCP_BUFFER_SIZE = 64 * 1024;
8
+ export class DnsHandler {
9
+ server;
10
+ udpServer = null;
11
+ tcpServer = null;
12
+ port;
13
+ fallbackDns;
14
+ config;
15
+ activeTcpConnections = new Map();
16
+ maxTcpConnections;
17
+ tcpIdleTimeoutMs;
18
+ rateLimiter;
19
+ constructor(server, config) {
20
+ this.server = server;
21
+ this.config = config;
22
+ this.port = config.port;
23
+ this.fallbackDns = config.fallbackDns;
24
+ this.maxTcpConnections = config.maxTcpConnections ?? 100;
25
+ this.tcpIdleTimeoutMs = config.tcpIdleTimeoutMs ?? 30000;
26
+ if (config.rateLimitMaxRequests && config.rateLimitMaxRequests > 0) {
27
+ this.rateLimiter = new RateLimiter({
28
+ maxRequests: config.rateLimitMaxRequests,
29
+ windowMs: config.rateLimitWindowMs ?? 1000,
30
+ });
31
+ }
32
+ else {
33
+ this.rateLimiter = null;
34
+ }
35
+ }
36
+ async start() {
37
+ await this.startUdp();
38
+ await this.startTcp();
39
+ }
40
+ async stop() {
41
+ if (this.udpServer) {
42
+ await new Promise((resolve) => {
43
+ this.udpServer?.close(() => resolve());
44
+ });
45
+ this.udpServer = null;
46
+ }
47
+ if (this.tcpServer) {
48
+ await new Promise((resolve) => {
49
+ this.tcpServer?.close(() => resolve());
50
+ });
51
+ this.tcpServer = null;
52
+ }
53
+ }
54
+ startUdp() {
55
+ return new Promise((resolve, reject) => {
56
+ const udp = dgram.createSocket("udp4");
57
+ udp.on("message", (data, rinfo) => {
58
+ this.handleUdpMessage(data, rinfo);
59
+ });
60
+ udp.on("error", (err) => {
61
+ if (err.code === "EACCES" || err.code === "EADDRINUSE") {
62
+ reject(err);
63
+ }
64
+ });
65
+ udp.on("listening", () => {
66
+ try {
67
+ udp.setRecvBufferSize(1024 * 1024);
68
+ }
69
+ catch { }
70
+ this.udpServer = udp;
71
+ resolve();
72
+ });
73
+ udp.bind(this.port, "0.0.0.0");
74
+ });
75
+ }
76
+ startTcp() {
77
+ return new Promise((resolve) => {
78
+ const tcpServer = net.createServer((socket) => {
79
+ if (this.activeTcpConnections.size >= this.maxTcpConnections) {
80
+ socket.destroy();
81
+ return;
82
+ }
83
+ this.activeTcpConnections.set(socket, { timeoutId: undefined });
84
+ socket.setTimeout(this.tcpIdleTimeoutMs);
85
+ socket.on("timeout", () => {
86
+ this.removeTcpConnection(socket);
87
+ socket.destroy();
88
+ });
89
+ this.handleTcpConnection(socket);
90
+ });
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
+ tcpServer.on("listening", () => {
99
+ this.tcpServer = tcpServer;
100
+ resolve();
101
+ });
102
+ tcpServer.listen(this.port, "0.0.0.0");
103
+ });
104
+ }
105
+ isRateLimited(ip) {
106
+ if (!this.rateLimiter)
107
+ return false;
108
+ return !this.rateLimiter.allow(ip);
109
+ }
110
+ parseResolvedIpv4s(resp) {
111
+ const ips = [];
112
+ try {
113
+ const f = new DnsWireFormat(resp);
114
+ f.offset = 4;
115
+ const qd = f.readUint16();
116
+ const an = f.readUint16();
117
+ f.offset += 4;
118
+ for (let i = 0; i < qd; i++) {
119
+ f.readDomainName();
120
+ f.offset += 4;
121
+ }
122
+ for (let i = 0; i < an; i++) {
123
+ f.readDomainName();
124
+ const type = f.readUint16();
125
+ f.offset += 6;
126
+ const len = f.readUint16();
127
+ if (type === 1 && len === 4) {
128
+ ips.push(`${resp[f.offset]}.${resp[f.offset + 1]}.${resp[f.offset + 2]}.${resp[f.offset + 3]}`);
129
+ }
130
+ f.offset += len;
131
+ }
132
+ }
133
+ catch { }
134
+ return ips;
135
+ }
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);
147
+ }
148
+ }, 3000);
149
+ fwdSocket.on("message", (resp) => {
150
+ if (!handled) {
151
+ handled = true;
152
+ clearTimeout(timeout);
153
+ const ips = this.parseResolvedIpv4s(resp);
154
+ let blockedIp = null;
155
+ for (const ip of ips) {
156
+ if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
157
+ blockedIp = ip;
158
+ break;
159
+ }
160
+ }
161
+ if (blockedIp) {
162
+ const ref = this.server.generateErrorResponse(data, DNS_RCODE.REFUSED);
163
+ this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
164
+ }
165
+ else {
166
+ this.udpServer?.send(resp, 0, resp.length, clientInfo.port, clientInfo.address);
167
+ }
168
+ fwdSocket.close();
169
+ }
170
+ });
171
+ fwdSocket.on("error", (err) => {
172
+ if (!handled) {
173
+ handled = true;
174
+ clearTimeout(timeout);
175
+ fwdSocket.close();
176
+ const ref = this.server.generateErrorResponse(data, DNS_RCODE.SERVFAIL);
177
+ this.udpServer?.send(ref, 0, ref.length, clientInfo.port, clientInfo.address);
178
+ }
179
+ });
180
+ fwdSocket.send(data, 0, data.length, 53, this.fallbackDns);
181
+ }
182
+ handleUdpMessage(data, rinfo) {
183
+ if (this.isRateLimited(rinfo.address)) {
184
+ return;
185
+ }
186
+ try {
187
+ const response = this.server.resolve(data, rinfo.address);
188
+ if (response) {
189
+ if (response.length > 0) {
190
+ this.udpServer?.send(response, 0, response.length, rinfo.port, rinfo.address);
191
+ }
192
+ }
193
+ else {
194
+ if (this.fallbackDns) {
195
+ this.forwardUdpQuery(data, rinfo);
196
+ }
197
+ else {
198
+ const nx = this.server.generateErrorResponse(data, DNS_RCODE.NXDOMAIN);
199
+ this.udpServer?.send(nx, 0, nx.length, rinfo.port, rinfo.address);
200
+ }
201
+ }
202
+ }
203
+ catch (err) {
204
+ }
205
+ }
206
+ removeTcpConnection(socket) {
207
+ const entry = this.activeTcpConnections.get(socket);
208
+ if (entry?.timeoutId)
209
+ clearTimeout(entry.timeoutId);
210
+ this.activeTcpConnections.delete(socket);
211
+ }
212
+ forwardTcpQuery(query, clientSocket, peerAddr) {
213
+ if (!this.fallbackDns)
214
+ return;
215
+ const fwd = net.createConnection(53, this.fallbackDns, () => {
216
+ const prefixed = Buffer.alloc(2 + query.length);
217
+ prefixed.writeUInt16BE(query.length, 0);
218
+ query.copy(prefixed, 2);
219
+ fwd.write(prefixed);
220
+ });
221
+ fwd.on("data", (data) => {
222
+ if (data.length < 2)
223
+ return;
224
+ const resp = data.subarray(2);
225
+ const ips = this.parseResolvedIpv4s(resp);
226
+ let blockedIp = null;
227
+ for (const ip of ips) {
228
+ if (firewallEngine.evaluateIp(ip, this.config.firewall) === "DENY") {
229
+ blockedIp = ip;
230
+ break;
231
+ }
232
+ }
233
+ if (blockedIp) {
234
+ if (!clientSocket.destroyed) {
235
+ const ref = this.server.generateErrorResponse(query, DNS_RCODE.REFUSED);
236
+ const p = Buffer.alloc(2 + ref.length);
237
+ p.writeUInt16BE(ref.length, 0);
238
+ ref.copy(p, 2);
239
+ clientSocket.write(p);
240
+ clientSocket.end();
241
+ }
242
+ }
243
+ else {
244
+ if (!clientSocket.destroyed)
245
+ clientSocket.write(data);
246
+ }
247
+ });
248
+ fwd.on("end", () => {
249
+ if (!clientSocket.destroyed)
250
+ clientSocket.end();
251
+ });
252
+ fwd.on("error", (err) => {
253
+ if (!clientSocket.destroyed) {
254
+ const sf = this.server.generateErrorResponse(query, DNS_RCODE.SERVFAIL);
255
+ const p = Buffer.alloc(2 + sf.length);
256
+ p.writeUInt16BE(sf.length, 0);
257
+ sf.copy(p, 2);
258
+ clientSocket.write(p);
259
+ clientSocket.end();
260
+ }
261
+ });
262
+ }
263
+ handleTcpConnection(socket) {
264
+ let buffer = Buffer.alloc(0);
265
+ const peerAddr = socket.remoteAddress || "unknown";
266
+ socket.on("close", () => this.removeTcpConnection(socket));
267
+ socket.on("error", (err) => {
268
+ this.removeTcpConnection(socket);
269
+ });
270
+ if (this.isRateLimited(peerAddr)) {
271
+ socket.destroy();
272
+ return;
273
+ }
274
+ socket.on("data", (data) => {
275
+ if (buffer.length + data.length > MAX_TCP_BUFFER_SIZE) {
276
+ socket.destroy();
277
+ return;
278
+ }
279
+ const combined = Buffer.concat([buffer, data]);
280
+ buffer = combined;
281
+ while (buffer.length >= 2) {
282
+ const length = buffer.readUInt16BE(0);
283
+ if (buffer.length < 2 + length)
284
+ continue;
285
+ const query = buffer.subarray(2, 2 + length);
286
+ buffer = buffer.subarray(2 + length);
287
+ try {
288
+ const response = this.server.resolve(query, peerAddr);
289
+ if (response) {
290
+ if (response.length > 0) {
291
+ const prefixed = Buffer.alloc(2 + response.length);
292
+ prefixed.writeUInt16BE(response.length, 0);
293
+ response.copy(prefixed, 2);
294
+ socket.write(prefixed);
295
+ }
296
+ else {
297
+ socket.end();
298
+ }
299
+ }
300
+ else {
301
+ if (this.fallbackDns) {
302
+ this.forwardTcpQuery(query, socket, peerAddr);
303
+ }
304
+ else {
305
+ const nx = this.server.generateErrorResponse(query, DNS_RCODE.NXDOMAIN);
306
+ const p = Buffer.alloc(2 + nx.length);
307
+ p.writeUInt16BE(nx.length, 0);
308
+ nx.copy(p, 2);
309
+ socket.write(p);
310
+ socket.end();
311
+ }
312
+ }
313
+ }
314
+ catch (err) {
315
+ socket.end();
316
+ }
317
+ }
318
+ });
319
+ }
320
+ getPort() {
321
+ return this.port;
322
+ }
323
+ }
@@ -0,0 +1,45 @@
1
+ import { ServerConfig } from "./types.js";
2
+ export declare class DnsWireFormat {
3
+ buf: Buffer;
4
+ offset: number;
5
+ constructor(buffer?: Buffer);
6
+ writeUint16(val: number): void;
7
+ writeUint8(val: number): void;
8
+ writeBytes(data: Uint8Array): void;
9
+ writeDomainName(name: string): void;
10
+ readUint16(): number;
11
+ readUint8(): number;
12
+ readDomainName(): string;
13
+ readUint32(): number;
14
+ finish(): Buffer;
15
+ writeUint32(val: number): void;
16
+ }
17
+ interface ParsedQuestion {
18
+ name: string;
19
+ type: number;
20
+ nextOffset: number;
21
+ }
22
+ export declare function extractQuestions(query: Buffer): ParsedQuestion[];
23
+ export declare class DevDnsServer {
24
+ private config;
25
+ private cacheMap;
26
+ private cacheOrder;
27
+ private dnsCacheMaxSize;
28
+ private dnsCacheTtlMs;
29
+ constructor(config: ServerConfig);
30
+ private normalizeHost;
31
+ private findHostConfig;
32
+ private generateCacheKey;
33
+ private evictCacheEntry;
34
+ private getFromCache;
35
+ private addToCache;
36
+ private skipQuestionSection;
37
+ private rewriteResponseTtl;
38
+ private buildSingleAnswer;
39
+ private buildAnswers;
40
+ generateErrorResponse(query: Buffer, rcode: number): Buffer;
41
+ resolve(query: Buffer, sourceIp?: string): Buffer | null;
42
+ hasRecord(name: string, type: number): boolean;
43
+ private toTypeNumber;
44
+ }
45
+ export {};