@opensecurity/zonzon-core 0.1.2 → 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.
- package/dist/audit.d.ts +10 -0
- package/dist/audit.js +39 -0
- package/dist/cache-layer.test.d.ts +1 -0
- package/dist/cache-layer.test.js +205 -0
- package/dist/cache-multi-question.test.d.ts +1 -0
- package/dist/cache-multi-question.test.js +187 -0
- package/dist/dns-handler.d.ts +27 -0
- package/dist/dns-handler.js +323 -0
- package/dist/dns-service.d.ts +45 -0
- package/dist/dns-service.js +546 -0
- package/dist/dns-service.test.d.ts +1 -0
- package/dist/dns-service.test.js +306 -0
- package/dist/dns-wireformat.test.d.ts +1 -0
- package/dist/dns-wireformat.test.js +669 -0
- package/dist/firewall.d.ts +9 -0
- package/dist/firewall.js +62 -0
- package/dist/http-body-forwarding-integration.test.d.ts +1 -0
- package/dist/http-body-forwarding-integration.test.js +318 -0
- package/dist/http-body-forwarding.test.d.ts +1 -0
- package/dist/http-body-forwarding.test.js +84 -0
- package/dist/http-handler.d.ts +21 -0
- package/dist/http-handler.js +429 -0
- package/dist/http-proxy.d.ts +14 -0
- package/dist/http-proxy.js +135 -0
- package/dist/http-proxy.test.d.ts +1 -0
- package/dist/http-proxy.test.js +375 -0
- package/{src/index.ts → dist/index.d.ts} +1 -1
- package/dist/index.js +10 -0
- package/dist/rate-limiter.d.ts +11 -0
- package/dist/rate-limiter.js +33 -0
- package/dist/rate-limiter.test.d.ts +1 -0
- package/dist/rate-limiter.test.js +149 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.js +124 -0
- package/dist/schema.test.d.ts +1 -0
- package/dist/schema.test.js +586 -0
- package/dist/sni-proxy.d.ts +12 -0
- package/dist/sni-proxy.js +141 -0
- package/dist/srv-record.test.d.ts +1 -0
- package/dist/srv-record.test.js +186 -0
- package/dist/tcp-connection-limit.test.d.ts +1 -0
- package/dist/tcp-connection-limit.test.js +89 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +34 -0
- package/dist/wildcard-matching.test.d.ts +1 -0
- package/dist/wildcard-matching.test.js +162 -0
- package/package.json +4 -1
- package/src/audit.ts +0 -43
- package/src/cache-layer.test.ts +0 -236
- package/src/cache-multi-question.test.ts +0 -263
- package/src/dns-handler.ts +0 -355
- package/src/dns-service.test.ts +0 -371
- package/src/dns-service.ts +0 -655
- package/src/dns-wireformat.test.ts +0 -771
- package/src/env.d.ts +0 -1
- package/src/firewall.ts +0 -66
- package/src/http-body-forwarding-integration.test.ts +0 -357
- package/src/http-body-forwarding.test.ts +0 -101
- package/src/http-handler.ts +0 -489
- package/src/http-proxy.test.ts +0 -440
- package/src/http-proxy.ts +0 -148
- package/src/rate-limiter.test.ts +0 -144
- package/src/rate-limiter.ts +0 -50
- package/src/schema.test.ts +0 -685
- package/src/schema.ts +0 -137
- package/src/sni-proxy.ts +0 -164
- package/src/srv-record.test.ts +0 -211
- package/src/tcp-connection-limit.test.ts +0 -110
- package/src/types.ts +0 -168
- package/src/wildcard-matching.test.ts +0 -196
- package/tsconfig.json +0 -9
|
@@ -1,263 +0,0 @@
|
|
|
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_CLASSES } from "./types.js";
|
|
5
|
-
|
|
6
|
-
describe("Multi-Question TTL Cache Rewriting", () => {
|
|
7
|
-
function buildMultiQuestionQuery(names: string[], types: number[]): Buffer {
|
|
8
|
-
const encoder = new (class {
|
|
9
|
-
buf = Buffer.alloc(1024);
|
|
10
|
-
offset = 0;
|
|
11
|
-
|
|
12
|
-
writeUint16(v: number) {
|
|
13
|
-
this.buf.writeUInt16BE(v, this.offset);
|
|
14
|
-
this.offset += 2;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
writeUint8(v: number) {
|
|
18
|
-
this.buf.writeUInt8(v, this.offset);
|
|
19
|
-
this.offset += 1;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
writeDomainName(nm: string) {
|
|
23
|
-
for (const label of nm.split(".")) {
|
|
24
|
-
if (label.length === 0) continue;
|
|
25
|
-
this.writeUint8(label.length);
|
|
26
|
-
Buffer.from(label).copy(this.buf, this.offset);
|
|
27
|
-
this.offset += label.length;
|
|
28
|
-
}
|
|
29
|
-
this.writeUint8(0);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
finish(): Buffer {
|
|
33
|
-
return this.buf.subarray(0, this.offset);
|
|
34
|
-
}
|
|
35
|
-
})();
|
|
36
|
-
|
|
37
|
-
encoder.writeUint16(0xCAFE);
|
|
38
|
-
encoder.writeUint16(0x0100);
|
|
39
|
-
encoder.writeUint16(names.length);
|
|
40
|
-
encoder.writeUint16(0);
|
|
41
|
-
encoder.writeUint16(0);
|
|
42
|
-
encoder.writeUint16(0);
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < names.length; i++) {
|
|
45
|
-
encoder.writeDomainName(names[i]);
|
|
46
|
-
encoder.writeUint16(types[i] || DNS_TYPES.A);
|
|
47
|
-
encoder.writeUint16(DNS_CLASSES.IN);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return encoder.finish();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function extractAnswerTtls(response: Buffer): number[] {
|
|
54
|
-
const ttls: number[] = [];
|
|
55
|
-
|
|
56
|
-
const qdcount = response.readUInt16BE(4);
|
|
57
|
-
let offset = 12;
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < qdcount && offset < response.length; i++) {
|
|
60
|
-
while (offset < response.length) {
|
|
61
|
-
const len = response[offset];
|
|
62
|
-
if (len === 0) { offset++; break; }
|
|
63
|
-
if ((len & 0xc0) === 0xc0) {
|
|
64
|
-
offset += 2;
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
offset += 1 + len;
|
|
68
|
-
}
|
|
69
|
-
offset += 4;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const ancount = response.readUInt16BE(6);
|
|
73
|
-
for (let i = 0; i < ancount && offset + 8 <= response.length; i++) {
|
|
74
|
-
offset += 2;
|
|
75
|
-
offset += 2;
|
|
76
|
-
offset += 2;
|
|
77
|
-
ttls.push(response.readUInt32BE(offset));
|
|
78
|
-
offset += 4;
|
|
79
|
-
|
|
80
|
-
const rdlength = response.readUInt16BE(offset);
|
|
81
|
-
offset += 2 + rdlength;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return ttls;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
it("rewrites TTL for all answer entries in a cached multi-question response", async () => {
|
|
88
|
-
const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
|
|
89
|
-
port: 53,
|
|
90
|
-
dnsCacheMaxSize: 100,
|
|
91
|
-
dnsCacheTtlMs: 10000,
|
|
92
|
-
hosts: {
|
|
93
|
-
"alpha.loop": { records: [{ type: "A", address: "1.1.1.1" }] },
|
|
94
|
-
"beta.loop": { records: [{ type: "A", address: "2.2.2.2" }] },
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const server = new DevDnsServer(config);
|
|
99
|
-
|
|
100
|
-
const query = buildMultiQuestionQuery(
|
|
101
|
-
["alpha.loop", "beta.loop"],
|
|
102
|
-
[DNS_TYPES.A, DNS_TYPES.A]
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const response1 = server.resolve(query)!;
|
|
106
|
-
assert.ok(response1.length > 0);
|
|
107
|
-
|
|
108
|
-
const ttlsAfterFirst = extractAnswerTtls(response1);
|
|
109
|
-
assert.strictEqual(ttlsAfterFirst.length, 2);
|
|
110
|
-
assert.ok(
|
|
111
|
-
ttlsAfterFirst.every((t) => t > 0)
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 200));
|
|
115
|
-
|
|
116
|
-
const response2 = server.resolve(query)!;
|
|
117
|
-
assert.ok(response2.length > 0);
|
|
118
|
-
|
|
119
|
-
const ttlsAfterCacheHit = extractAnswerTtls(response2);
|
|
120
|
-
assert.strictEqual(ttlsAfterCacheHit.length, 2);
|
|
121
|
-
|
|
122
|
-
const originalTotalTtl = ttlsAfterFirst.reduce((a, b) => a + b, 0);
|
|
123
|
-
const cachedTotalTtl = ttlsAfterCacheHit.reduce((a, b) => a + b, 0);
|
|
124
|
-
|
|
125
|
-
assert.ok(
|
|
126
|
-
cachedTotalTtl < originalTotalTtl
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
assert.ok(
|
|
130
|
-
ttlsAfterCacheHit.every((t) => t >= 0)
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("handles three-question multi-query without TTL corruption", async () => {
|
|
135
|
-
const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
|
|
136
|
-
port: 53,
|
|
137
|
-
dnsCacheMaxSize: 100,
|
|
138
|
-
dnsCacheTtlMs: 10000,
|
|
139
|
-
hosts: {
|
|
140
|
-
"one.loop": { records: [{ type: "A", address: "1.0.0.1" }] },
|
|
141
|
-
"two.loop": { records: [{ type: "A", address: "1.0.0.2" }] },
|
|
142
|
-
"three.loop": { records: [{ type: "A", address: "1.0.0.3" }] },
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const server = new DevDnsServer(config);
|
|
147
|
-
|
|
148
|
-
const query = buildMultiQuestionQuery(
|
|
149
|
-
["one.loop", "two.loop", "three.loop"],
|
|
150
|
-
[DNS_TYPES.A, DNS_TYPES.A, DNS_TYPES.A]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const response1 = server.resolve(query)!;
|
|
154
|
-
assert.ok(response1.length > 0);
|
|
155
|
-
|
|
156
|
-
const ttlsAfterFirst = extractAnswerTtls(response1);
|
|
157
|
-
assert.strictEqual(ttlsAfterFirst.length, 3);
|
|
158
|
-
|
|
159
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 300));
|
|
160
|
-
const response2 = server.resolve(query)!;
|
|
161
|
-
assert.ok(response2.length > 0);
|
|
162
|
-
|
|
163
|
-
const ttlsAfterCache = extractAnswerTtls(response2);
|
|
164
|
-
assert.strictEqual(ttlsAfterCache.length, 3);
|
|
165
|
-
|
|
166
|
-
const maxTtl = config.dnsCacheTtlMs! / 1000;
|
|
167
|
-
for (const ttl of ttlsAfterCache) {
|
|
168
|
-
assert.ok(
|
|
169
|
-
ttl >= 0 && ttl <= maxTtl
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("does not corrupt question data when rewriting TTL in multi-question response", async () => {
|
|
175
|
-
const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
|
|
176
|
-
port: 53,
|
|
177
|
-
dnsCacheMaxSize: 100,
|
|
178
|
-
dnsCacheTtlMs: 10000,
|
|
179
|
-
hosts: {
|
|
180
|
-
"a.loop": { records: [{ type: "A", address: "1.2.3.4" }] },
|
|
181
|
-
"b.loop": { records: [{ type: "A", address: "5.6.7.8" }] },
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const server = new DevDnsServer(config);
|
|
186
|
-
|
|
187
|
-
const query = buildMultiQuestionQuery(
|
|
188
|
-
["a.loop", "b.loop"],
|
|
189
|
-
[DNS_TYPES.A, DNS_TYPES.A]
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const originalResponse = server.resolve(query)!;
|
|
193
|
-
assert.ok(originalResponse.length > 0);
|
|
194
|
-
|
|
195
|
-
const originalBytes = Buffer.from(originalResponse);
|
|
196
|
-
|
|
197
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 150));
|
|
198
|
-
const cachedResponse = server.resolve(query)!;
|
|
199
|
-
assert.ok(cachedResponse.length > 0);
|
|
200
|
-
|
|
201
|
-
assert.strictEqual(
|
|
202
|
-
cachedResponse.readUInt16BE(6),
|
|
203
|
-
originalResponse.readUInt16BE(6)
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
assert.strictEqual(
|
|
207
|
-
cachedResponse.length,
|
|
208
|
-
originalBytes.length
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
const origQdcount = originalResponse.readUInt16BE(4);
|
|
212
|
-
const cacheQdcount = cachedResponse.readUInt16BE(4);
|
|
213
|
-
assert.strictEqual(origQdcount, cacheQdcount);
|
|
214
|
-
|
|
215
|
-
const origAncount = originalResponse.readUInt16BE(6);
|
|
216
|
-
const cacheAncount = cachedResponse.readUInt16BE(6);
|
|
217
|
-
assert.strictEqual(origAncount, cacheAncount);
|
|
218
|
-
|
|
219
|
-
const origTtls = extractAnswerTtls(originalResponse);
|
|
220
|
-
const cacheTtls = extractAnswerTtls(cachedResponse);
|
|
221
|
-
for (let i = 0; i < origTtls.length; i++) {
|
|
222
|
-
assert.ok(
|
|
223
|
-
cacheTtls[i] <= origTtls[i]
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("handles multi-question with mismatched record types", async () => {
|
|
229
|
-
const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
|
|
230
|
-
port: 53,
|
|
231
|
-
dnsCacheMaxSize: 100,
|
|
232
|
-
dnsCacheTtlMs: 10000,
|
|
233
|
-
hosts: {
|
|
234
|
-
"a.loop": { records: [{ type: "A", address: "1.2.3.4" }] },
|
|
235
|
-
"b.loop": { records: [{ type: "AAAA", address: "::1" }] },
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const server = new DevDnsServer(config);
|
|
240
|
-
|
|
241
|
-
const query = buildMultiQuestionQuery(
|
|
242
|
-
["a.loop", "b.loop"],
|
|
243
|
-
[DNS_TYPES.A, DNS_TYPES.AAAA]
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
const response1 = server.resolve(query)!;
|
|
247
|
-
assert.ok(response1.length > 0);
|
|
248
|
-
|
|
249
|
-
const ttlsFirst = extractAnswerTtls(response1);
|
|
250
|
-
assert.strictEqual(ttlsFirst.length, 2);
|
|
251
|
-
|
|
252
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 100));
|
|
253
|
-
const response2 = server.resolve(query)!;
|
|
254
|
-
assert.ok(response2.length > 0);
|
|
255
|
-
|
|
256
|
-
const ttlsCache = extractAnswerTtls(response2);
|
|
257
|
-
assert.strictEqual(ttlsCache.length, 2);
|
|
258
|
-
|
|
259
|
-
for (const ttl of ttlsCache) {
|
|
260
|
-
assert.ok(ttl >= 0);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
});
|
package/src/dns-handler.ts
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
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
|
-
}
|