@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,144 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "assert";
|
|
3
|
+
import * as net from "net";
|
|
4
|
+
|
|
5
|
+
let PORT_BASE = 65000;
|
|
6
|
+
function nextPort(): number { return PORT_BASE++; }
|
|
7
|
+
|
|
8
|
+
describe("TCP Rate Limiting", () => {
|
|
9
|
+
function buildTcpDnsQuery(name: string): Buffer {
|
|
10
|
+
const encoder = new (class {
|
|
11
|
+
buf = Buffer.alloc(256); offset = 0;
|
|
12
|
+
writeUint16(v: number) { this.buf.writeUInt16BE(v, this.offset); this.offset += 2; }
|
|
13
|
+
writeUint8(v: number) { this.buf.writeUInt8(v, this.offset); this.offset += 1; }
|
|
14
|
+
writeDomainName(nm: string) {
|
|
15
|
+
for (const label of nm.split(".")) { if (!label.length) continue; this.writeUint8(label.length); Buffer.from(label).copy(this.buf, this.offset); this.offset += label.length; }
|
|
16
|
+
this.writeUint8(0);
|
|
17
|
+
}
|
|
18
|
+
finish(): Buffer { return this.buf.subarray(0, this.offset); }
|
|
19
|
+
})();
|
|
20
|
+
encoder.writeUint16(0xDEAD); encoder.writeUint16(0x0100); encoder.writeUint16(1);
|
|
21
|
+
encoder.writeUint16(0); encoder.writeUint16(0); encoder.writeUint16(0);
|
|
22
|
+
encoder.writeDomainName(name); encoder.writeUint16(1); encoder.writeUint16(1);
|
|
23
|
+
const query = encoder.finish();
|
|
24
|
+
const prefixed = Buffer.alloc(2 + query.length);
|
|
25
|
+
prefixed.writeUInt16BE(query.length, 0); query.copy(prefixed, 2);
|
|
26
|
+
return prefixed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
it("TCP queries are rate limited when configured", async () => {
|
|
30
|
+
const port = nextPort();
|
|
31
|
+
const { DnsHandler } = await import("./dns-handler.js");
|
|
32
|
+
const { DevDnsServer } = await import("./dns-service.js");
|
|
33
|
+
const config: any = { port, hosts: { "test.loop": { records: [{ type: "A", address: "5.6.7.8" }] } }, rateLimitMaxRequests: 3, rateLimitWindowMs: 1000 };
|
|
34
|
+
|
|
35
|
+
const dnsServer = new DevDnsServer(config);
|
|
36
|
+
const handler = new DnsHandler(dnsServer, config);
|
|
37
|
+
await handler.start();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await new Promise<void>((r) => setTimeout(r, 150));
|
|
41
|
+
|
|
42
|
+
const results: number[] = [];
|
|
43
|
+
for (let i = 0; i < 3; i++) {
|
|
44
|
+
let responses = 0;
|
|
45
|
+
await new Promise<void>((resolve) => {
|
|
46
|
+
const socket = net.createConnection(port, "127.0.0.1", () => {
|
|
47
|
+
socket.write(buildTcpDnsQuery("test.loop"));
|
|
48
|
+
socket.on("data", (data: Buffer) => {
|
|
49
|
+
let off = 0;
|
|
50
|
+
while (off + 2 <= data.length) {
|
|
51
|
+
const len = data.readUInt16BE(off);
|
|
52
|
+
if (len === 0 || off + 2 + len > data.length) break;
|
|
53
|
+
responses++; off += 2 + len;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
setTimeout(() => { socket.destroy(); resolve(); }, 1000);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
results.push(responses);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
assert.ok(results.every((r) => r > 0));
|
|
63
|
+
} finally {
|
|
64
|
+
await handler.stop();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("TCP connection is terminated when source IP exceeds rate limit", async () => {
|
|
69
|
+
const port = nextPort();
|
|
70
|
+
const { DnsHandler } = await import("./dns-handler.js");
|
|
71
|
+
const { DevDnsServer } = await import("./dns-service.js");
|
|
72
|
+
const config: any = { port, hosts: { "test.loop": { records: [{ type: "A", address: "9.8.7.6" }] } }, rateLimitMaxRequests: 1, rateLimitWindowMs: 5000 };
|
|
73
|
+
|
|
74
|
+
const dnsServer = new DevDnsServer(config);
|
|
75
|
+
const handler = new DnsHandler(dnsServer, config);
|
|
76
|
+
await handler.start();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await new Promise<void>((r) => setTimeout(r, 150));
|
|
80
|
+
|
|
81
|
+
let r1Responses = 0;
|
|
82
|
+
await new Promise<void>((resolve) => {
|
|
83
|
+
const socket = net.createConnection(port, "127.0.0.1", () => {
|
|
84
|
+
socket.write(buildTcpDnsQuery("test.loop"));
|
|
85
|
+
socket.on("data", (data: Buffer) => {
|
|
86
|
+
let off = 0;
|
|
87
|
+
while (off + 2 <= data.length) {
|
|
88
|
+
const len = data.readUInt16BE(off);
|
|
89
|
+
if (len === 0 || off + 2 + len > data.length) break;
|
|
90
|
+
r1Responses++; off += 2 + len;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
setTimeout(() => { socket.destroy(); resolve(); }, 1000);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
assert.ok(r1Responses > 0);
|
|
97
|
+
|
|
98
|
+
let r2Destroyed = false;
|
|
99
|
+
await new Promise<void>((resolve) => {
|
|
100
|
+
const socket = net.createConnection(port, "127.0.0.1", () => {
|
|
101
|
+
setTimeout(() => { socket.destroy(); resolve(); }, 1500);
|
|
102
|
+
});
|
|
103
|
+
socket.on("error", () => { r2Destroyed = true; });
|
|
104
|
+
socket.on("close", () => { if (!r2Destroyed) r2Destroyed = true; });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
assert.strictEqual(r2Destroyed, true);
|
|
108
|
+
} finally {
|
|
109
|
+
await handler.stop();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("TCP queries work when rate limiting is disabled", async () => {
|
|
114
|
+
const port = nextPort();
|
|
115
|
+
const { DnsHandler } = await import("./dns-handler.js");
|
|
116
|
+
const { DevDnsServer } = await import("./dns-service.js");
|
|
117
|
+
const config: any = { port, hosts: { "test.loop": { records: [{ type: "A", address: "1.2.3.4" }] } }, rateLimitMaxRequests: 0 };
|
|
118
|
+
|
|
119
|
+
const dnsServer = new DevDnsServer(config);
|
|
120
|
+
const handler = new DnsHandler(dnsServer, config);
|
|
121
|
+
await handler.start();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await new Promise<void>((r) => setTimeout(r, 150));
|
|
125
|
+
|
|
126
|
+
let successes = 0;
|
|
127
|
+
for (let i = 0; i < 5; i++) {
|
|
128
|
+
let gotResponse = false;
|
|
129
|
+
await new Promise<void>((resolve) => {
|
|
130
|
+
const socket = net.createConnection(port, "127.0.0.1", () => {
|
|
131
|
+
socket.write(buildTcpDnsQuery("test.loop"));
|
|
132
|
+
socket.on("data", () => { gotResponse = true; });
|
|
133
|
+
setTimeout(() => { socket.destroy(); resolve(); }, 1000);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
if (gotResponse) successes++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(successes, 5);
|
|
140
|
+
} finally {
|
|
141
|
+
await handler.stop();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface RateLimiterOptions {
|
|
2
|
+
maxRequests: number;
|
|
3
|
+
windowMs: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface IpBucket {
|
|
7
|
+
timestamps: number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RateLimiter {
|
|
11
|
+
private options: RateLimiterOptions;
|
|
12
|
+
private buckets = new Map<string, IpBucket>();
|
|
13
|
+
|
|
14
|
+
constructor(options: RateLimiterOptions) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
allow(ip: string): boolean {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
let bucket = this.buckets.get(ip);
|
|
21
|
+
|
|
22
|
+
if (!bucket || now - bucket.timestamps[0] > this.options.windowMs) {
|
|
23
|
+
bucket = { timestamps: [now] };
|
|
24
|
+
this.buckets.set(ip, bucket);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const windowStart = now - this.options.windowMs;
|
|
29
|
+
while (bucket.timestamps.length > 0 && bucket.timestamps[0] < windowStart) {
|
|
30
|
+
bucket.timestamps.shift();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (bucket.timestamps.length >= this.options.maxRequests) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
bucket.timestamps.push(now);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRequestCount(ip: string): number {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const windowStart = now - this.options.windowMs;
|
|
44
|
+
const bucket = this.buckets.get(ip);
|
|
45
|
+
|
|
46
|
+
if (!bucket) return 0;
|
|
47
|
+
|
|
48
|
+
return bucket.timestamps.filter((t) => t >= windowStart).length;
|
|
49
|
+
}
|
|
50
|
+
}
|