@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/src/schema.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ import * as net from "net";
3
+ import { HostConfig, DnsRecord, ServerConfig } from "./types.js";
4
+
5
+ const CrlfFreeString = z.string().refine((val) => !/[\r\n]/.test(val), "Contains CR/LF");
6
+
7
+ const Ipv4Schema = z.string().refine((ip) => net.isIPv4(ip), "Invalid IPv4");
8
+ const Ipv6Schema = z.string().refine((ip) => net.isIPv6(ip), "Invalid IPv6");
9
+
10
+ const HostnameSchema = z.string().max(253).refine((hostname) => {
11
+ const parts = hostname.split(".");
12
+ const hostPattern = /^[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?$/;
13
+ return parts.every((part) => part.length > 0 && part.length <= 63 && hostPattern.test(part));
14
+ }, "Invalid hostname");
15
+
16
+ const PortSchema = z.number().int().min(1).max(65535);
17
+
18
+ const ARecordSchema = z.object({ type: z.literal("A"), address: Ipv4Schema });
19
+ const AAAARecordSchema = z.object({ type: z.literal("AAAA"), address: Ipv6Schema });
20
+ const CNAMERecordSchema = z.object({ type: z.literal("CNAME"), target: HostnameSchema });
21
+ const TXTRecordSchema = z.object({ type: z.literal("TXT"), data: z.array(z.string().max(255).refine((val) => !/[\r\n]/.test(val), "Contains CR/LF")) });
22
+ const MXRecordSchema = z.object({ type: z.literal("MX"), priority: z.number().int().min(0).max(65535), exchange: HostnameSchema });
23
+ const NSRecordSchema = z.object({ type: z.literal("NS"), target: HostnameSchema });
24
+ const SRVRecordSchema = z.object({ type: z.literal("SRV"), priority: z.number().int().min(0).max(65535), weight: z.number().int().min(0).max(65535), port: PortSchema, target: HostnameSchema });
25
+ const PTRRecordSchema = z.object({ type: z.literal("PTR"), target: HostnameSchema });
26
+
27
+ const DnsRecordSchema = z.discriminatedUnion("type", [
28
+ ARecordSchema,
29
+ AAAARecordSchema,
30
+ CNAMERecordSchema,
31
+ TXTRecordSchema,
32
+ MXRecordSchema,
33
+ NSRecordSchema,
34
+ SRVRecordSchema,
35
+ PTRRecordSchema,
36
+ ]);
37
+
38
+ const HttpProxySchema = z.object({
39
+ enabled: z.boolean(),
40
+ upstream: CrlfFreeString.optional(),
41
+ headers: z.record(z.string().regex(/^[a-zA-Z0-9\-]+$/), CrlfFreeString).default({}),
42
+ forwardRequestBody: z.boolean().default(false),
43
+ maxRequestBodyBytes: z.number().int().min(0).max(10485760).default(5242880),
44
+ }).refine(data => !data.enabled || !!data.upstream, {
45
+ message: "HTTP proxy requires 'upstream' URL when enabled",
46
+ path: ["upstream"]
47
+ });
48
+
49
+ const RedirectSchema = z.object({
50
+ enabled: z.boolean().default(true),
51
+ code: z.union([z.literal(301), z.literal(302), z.literal(303), z.literal(307), z.literal(308)]),
52
+ target: CrlfFreeString,
53
+ });
54
+
55
+ const HostConfigSchema = z.object({
56
+ records: z.array(DnsRecordSchema).default([]),
57
+ http_proxy: HttpProxySchema.optional(),
58
+ redirect: RedirectSchema.optional(),
59
+ });
60
+
61
+ const FirewallSchema = z.object({
62
+ defaultPolicy: z.enum(["allow", "deny"]).default("deny"),
63
+ allowlist_domains: z.array(z.string().max(253)).default([]),
64
+ blocklist_domains: z.array(z.string().max(253)).default([]),
65
+ allowlist_ranges: z.array(z.string().max(40)).default([]),
66
+ blocklist_ranges: z.array(z.string().max(40)).default([]),
67
+ allowlist_ips: z.array(z.string().max(40)).default([]),
68
+ blocklist_ips: z.array(z.string().max(40)).default([]),
69
+ });
70
+
71
+ const ControlPlaneSchema = z.object({
72
+ enabled: z.boolean().default(true).optional(),
73
+ port: z.coerce.number().int().min(1).max(65535).default(8080).optional(),
74
+ apiKey: z.string().optional()
75
+ });
76
+
77
+ const ServerConfigSchema = z.object({
78
+ port: z.coerce.number().int().min(1).max(65535).default(53),
79
+ fallbackDns: Ipv4Schema.optional(),
80
+ firewall: FirewallSchema.optional(),
81
+ controlPlane: ControlPlaneSchema.optional(),
82
+ dnsCacheMaxSize: z.coerce.number().int().min(1).max(100000).default(1024),
83
+ dnsCacheTtlMs: z.coerce.number().int().min(0).max(3600000).default(0),
84
+ maxTcpConnections: z.coerce.number().int().min(1).max(10000).default(100),
85
+ tcpIdleTimeoutMs: z.coerce.number().int().min(1000).max(600000).default(30000),
86
+ rateLimitMaxRequests: z.coerce.number().int().min(0).max(100000).default(0),
87
+ rateLimitWindowMs: z.coerce.number().int().min(100).max(60000).default(1000),
88
+ hosts: z.record(z.string(), HostConfigSchema).default({}),
89
+ }).refine(data => {
90
+ for (const key of Object.keys(data.hosts)) {
91
+ const normalized = key.toLowerCase();
92
+ if (normalized === "*") continue;
93
+ if (normalized.startsWith("*.")) {
94
+ HostnameSchema.parse(normalized.slice(2));
95
+ } else {
96
+ HostnameSchema.parse(normalized);
97
+ }
98
+ }
99
+ return true;
100
+ }, "Contains invalid hostnames in configuration keys");
101
+
102
+ export function validateARecord(record: unknown): DnsRecord { return ARecordSchema.parse(record) as DnsRecord; }
103
+ export function validateAAAARecord(record: unknown): DnsRecord { return AAAARecordSchema.parse(record) as DnsRecord; }
104
+ export function validateCNAME(record: unknown): DnsRecord { return CNAMERecordSchema.parse(record) as DnsRecord; }
105
+ export function validateTXT(record: unknown): DnsRecord { return TXTRecordSchema.parse(record) as DnsRecord; }
106
+ export function validateMX(record: unknown): DnsRecord { return MXRecordSchema.parse(record) as DnsRecord; }
107
+ export function validateNS(record: unknown): DnsRecord { return NSRecordSchema.parse(record) as DnsRecord; }
108
+ export function validateSRV(record: unknown): DnsRecord { return SRVRecordSchema.parse(record) as DnsRecord; }
109
+ export function validatePTR(record: unknown): DnsRecord { return PTRRecordSchema.parse(record) as DnsRecord; }
110
+
111
+ export function validateRecord(record: unknown): DnsRecord {
112
+ if (!record || typeof record !== "object" || !("type" in record)) {
113
+ throw new Error("DNS record must be an object with a 'type' field");
114
+ }
115
+ return DnsRecordSchema.parse(record) as DnsRecord;
116
+ }
117
+
118
+ export function validateHostConfig(config: unknown): HostConfig {
119
+ try {
120
+ return HostConfigSchema.parse(config) as HostConfig;
121
+ } catch (error) {
122
+ throw new Error(`Host validation error: ${error}`);
123
+ }
124
+ }
125
+
126
+ export function validateServerConfig(config: unknown): ServerConfig {
127
+ try {
128
+ const parsed = ServerConfigSchema.parse(config);
129
+ const lowercaseHosts: Record<string, HostConfig> = {};
130
+ for (const [key, value] of Object.entries(parsed.hosts)) {
131
+ lowercaseHosts[key.toLowerCase()] = value as HostConfig;
132
+ }
133
+ return { ...parsed, hosts: lowercaseHosts } as ServerConfig;
134
+ } catch (error) {
135
+ throw new Error(`Configuration validation error: ${error}`);
136
+ }
137
+ }
@@ -0,0 +1,164 @@
1
+ import * as net from "net";
2
+ import * as dns from "dns/promises";
3
+ import { ServerConfig } from "./types.js";
4
+ import { firewallEngine } from "./firewall.js";
5
+ import { audit } from "./audit.js";
6
+
7
+ export class SniProxyService {
8
+ private port: number;
9
+ private config: ServerConfig;
10
+ private server: net.Server | null = null;
11
+ private activeConnections = new Set<net.Socket>();
12
+
13
+ constructor(config: ServerConfig, port: number = 443) {
14
+ this.config = config;
15
+ this.port = port;
16
+ }
17
+
18
+ private extractSNI(data: Buffer): string | null {
19
+ try {
20
+ if (data.length < 5 || data[0] !== 0x16 || data[5] !== 0x01) {
21
+ return null;
22
+ }
23
+
24
+ let offset = 43;
25
+ if (offset >= data.length) return null;
26
+
27
+ const sessionIdLength = data[offset];
28
+ offset += 1 + sessionIdLength;
29
+ if (offset >= data.length) return null;
30
+
31
+ const cipherSuitesLength = data.readUInt16BE(offset);
32
+ offset += 2 + cipherSuitesLength;
33
+ if (offset >= data.length) return null;
34
+
35
+ const compressionMethodsLength = data[offset];
36
+ offset += 1 + compressionMethodsLength;
37
+ if (offset >= data.length) return null;
38
+
39
+ const extensionsLength = data.readUInt16BE(offset);
40
+ offset += 2;
41
+ const extensionsEnd = offset + extensionsLength;
42
+
43
+ while (offset < extensionsEnd && offset + 4 <= data.length) {
44
+ const extType = data.readUInt16BE(offset);
45
+ const extLength = data.readUInt16BE(offset + 2);
46
+ offset += 4;
47
+
48
+ if (extType === 0x0000) {
49
+ let sniOffset = offset;
50
+ sniOffset += 2;
51
+
52
+ const nameType = data[sniOffset];
53
+ if (nameType === 0) {
54
+ sniOffset += 1;
55
+ const nameLength = data.readUInt16BE(sniOffset);
56
+ sniOffset += 2;
57
+ return data.toString("utf8", sniOffset, sniOffset + nameLength);
58
+ }
59
+ }
60
+ offset += extLength;
61
+ }
62
+ } catch {
63
+ return null;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ private async handleConnection(clientSocket: net.Socket): Promise<void> {
69
+ const clientIp = clientSocket.remoteAddress || "unknown";
70
+ let buffer = Buffer.alloc(0);
71
+ let isHandled = false;
72
+
73
+ clientSocket.on("data", async (chunk: Buffer) => {
74
+ if (isHandled) return;
75
+
76
+ buffer = Buffer.concat([buffer, chunk]);
77
+
78
+ if (buffer.length < 5) return;
79
+ const recordLength = buffer.readUInt16BE(3);
80
+ if (buffer.length < 5 + recordLength) return;
81
+
82
+ isHandled = true;
83
+ const sni = this.extractSNI(buffer);
84
+
85
+ if (!sni) {
86
+ audit.http(clientIp, "TLS", "UNKNOWN", ":443", 400, "Dropped: No SNI detected");
87
+ clientSocket.destroy();
88
+ return;
89
+ }
90
+
91
+ try {
92
+ if (firewallEngine.evaluateDomain(sni, this.config.firewall) === "DENY") {
93
+ throw new Error("Domain blocked by Firewall policy");
94
+ }
95
+
96
+ const records = await dns.resolve(sni);
97
+ const targetIps = records.filter(ip => typeof ip === "string");
98
+
99
+ if (targetIps.length === 0) {
100
+ throw new Error("NXDOMAIN on upstream resolution");
101
+ }
102
+
103
+ const targetIp = targetIps[0];
104
+ if (firewallEngine.evaluateIp(targetIp, this.config.firewall) === "DENY") {
105
+ throw new Error(`Target IP ${targetIp} blocked by Firewall policy`);
106
+ }
107
+
108
+ audit.http(clientIp, "TLS-SNI", sni, ":443", 200, `Tunneled to ${targetIp}`);
109
+
110
+ const srvSocket = net.connect(443, targetIp, () => {
111
+ srvSocket.write(buffer);
112
+ clientSocket.pipe(srvSocket);
113
+ srvSocket.pipe(clientSocket);
114
+ });
115
+
116
+ this.activeConnections.add(srvSocket);
117
+ srvSocket.on("close", () => this.activeConnections.delete(srvSocket));
118
+
119
+ srvSocket.on("error", (err) => {
120
+ audit.error(`Upstream tunnel fault on ${sni}:443 - ${err.message}`);
121
+ if (!clientSocket.destroyed) clientSocket.destroy();
122
+ });
123
+
124
+ } catch (err: any) {
125
+ audit.http(clientIp, "TLS-SNI", sni, ":443", 403, `Blocked: ${err.message}`);
126
+ clientSocket.destroy();
127
+ }
128
+ });
129
+
130
+ clientSocket.on("error", (err) => {
131
+ audit.error(`Client tunnel fault from ${clientIp} - ${err.message}`);
132
+ });
133
+ }
134
+
135
+ public start(): Promise<void> {
136
+ return new Promise((resolve, reject) => {
137
+ this.server = net.createServer((socket: net.Socket) => {
138
+ this.activeConnections.add(socket);
139
+ socket.on("close", () => this.activeConnections.delete(socket));
140
+ this.handleConnection(socket);
141
+ });
142
+
143
+ this.server.on("error", (err) => reject(err));
144
+
145
+ this.server.listen(this.port, "0.0.0.0", () => {
146
+ resolve();
147
+ });
148
+ });
149
+ }
150
+
151
+ public async stop(): Promise<void> {
152
+ if (this.server) {
153
+ for (const socket of this.activeConnections) {
154
+ socket.destroy();
155
+ }
156
+ this.activeConnections.clear();
157
+
158
+ await new Promise<void>((resolve) => {
159
+ this.server!.close(() => resolve());
160
+ });
161
+ this.server = null;
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,211 @@
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(nm: string) {
22
+ for (const label of nm.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(0xabcd);
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 parseResponseFlags(buf: Buffer): { qr: number; rcode: number } {
51
+ const flags = buf.readUInt16BE(2);
52
+ const qr = (flags >> 15) & 0x1;
53
+ const rcode = flags & 0xf;
54
+ return { qr, rcode };
55
+ }
56
+
57
+ function getAnswerCount(buf: Buffer): number {
58
+ return buf.readUInt16BE(6);
59
+ }
60
+
61
+ describe("DevDnsServer - SRV Wire Format Encoding", () => {
62
+ let server: DevDnsServer;
63
+
64
+ it("returns correctly encoded SRV record with priority=10 weight=5 port=8080", () => {
65
+ const config: ServerConfig = {
66
+ port: 53,
67
+ hosts: {
68
+ "my.service.loop": {
69
+ records: [
70
+ { type: "SRV", priority: 10, weight: 5, port: 8080, target: "backend.my.service.loop" },
71
+ ],
72
+ },
73
+ },
74
+ };
75
+
76
+ server = new DevDnsServer(config);
77
+ const queryBuffer = buildQuery("my.service.loop", DNS_TYPES.SRV);
78
+ const response = server.resolve(queryBuffer)!;
79
+
80
+ assert.ok(response.length > 0);
81
+
82
+ const { qr, rcode } = parseResponseFlags(response);
83
+ assert.strictEqual(qr, 1);
84
+ assert.strictEqual(rcode, DNS_RCODE.NOERROR);
85
+
86
+ const ancount = getAnswerCount(response);
87
+ assert.strictEqual(ancount, 1);
88
+ assert.ok(response.length > 40);
89
+ const rdlen = response.readUInt16BE(20);
90
+ assert.ok(rdlen >= 6);
91
+ });
92
+
93
+ it("returns correctly encoded SRV record with zero priority and weight", () => {
94
+ const config: ServerConfig = {
95
+ port: 53,
96
+ hosts: {
97
+ "zero.srv.loop": {
98
+ records: [
99
+ { type: "SRV", priority: 0, weight: 0, port: 443, target: "primary.zero.srv.loop" },
100
+ ],
101
+ },
102
+ },
103
+ };
104
+
105
+ server = new DevDnsServer(config);
106
+ const response = server.resolve(buildQuery("zero.srv.loop", DNS_TYPES.SRV))!;
107
+
108
+ assert.ok(response.length > 0);
109
+ const ancount = getAnswerCount(response);
110
+ assert.strictEqual(ancount, 1);
111
+ });
112
+
113
+ it("returns multiple SRV records for same host (weighted load balancing)", () => {
114
+ const config: ServerConfig = {
115
+ port: 53,
116
+ hosts: {
117
+ "lb.srv.loop": {
118
+ records: [
119
+ { type: "SRV", priority: 10, weight: 7, port: 8080, target: "server1.lb.srv.loop" },
120
+ { type: "SRV", priority: 20, weight: 3, port: 8080, target: "server2.lb.srv.loop" },
121
+ ],
122
+ },
123
+ },
124
+ };
125
+
126
+ server = new DevDnsServer(config);
127
+ const response = server.resolve(buildQuery("lb.srv.loop", DNS_TYPES.SRV))!;
128
+
129
+ assert.ok(response.length > 0);
130
+ const ancount = getAnswerCount(response);
131
+ assert.strictEqual(ancount, 2);
132
+ });
133
+
134
+ it("does not respond with A record type when host only has SRV records", () => {
135
+ const config: ServerConfig = {
136
+ port: 53,
137
+ hosts: {
138
+ "only-srv.loop": {
139
+ records: [
140
+ { type: "SRV", priority: 10, weight: 5, port: 8080, target: "target.only-srv.loop" },
141
+ ],
142
+ },
143
+ },
144
+ };
145
+
146
+ server = new DevDnsServer(config);
147
+ const response = server.resolve(buildQuery("only-srv.loop", DNS_TYPES.A))!;
148
+
149
+ assert.ok(response.length > 0);
150
+ assert.strictEqual(getAnswerCount(response), 0);
151
+ const { rcode } = parseResponseFlags(response);
152
+ assert.strictEqual(rcode, DNS_RCODE.NOERROR);
153
+ });
154
+
155
+ it("handles SRV with maximum port value 65535", () => {
156
+ const config: ServerConfig = {
157
+ port: 53,
158
+ hosts: {
159
+ "maxport.srv.loop": {
160
+ records: [
161
+ { type: "SRV", priority: 100, weight: 200, port: 65535, target: "max.maxport.srv.loop" },
162
+ ],
163
+ },
164
+ },
165
+ };
166
+
167
+ server = new DevDnsServer(config);
168
+ const response = server.resolve(buildQuery("maxport.srv.loop", DNS_TYPES.SRV))!;
169
+
170
+ assert.ok(response.length > 0);
171
+ const ancount = getAnswerCount(response);
172
+ assert.strictEqual(ancount, 1);
173
+ });
174
+
175
+ it("preserves query ID in SRV response", () => {
176
+ const config: ServerConfig = {
177
+ port: 53,
178
+ hosts: {
179
+ "id.srv.loop": {
180
+ records: [
181
+ { type: "SRV", priority: 10, weight: 5, port: 8080, target: "target.id.srv.loop" },
182
+ ],
183
+ },
184
+ },
185
+ };
186
+
187
+ const testEncoder = new (class {
188
+ buf = Buffer.alloc(256);
189
+ offset = 0;
190
+ writeUint16(v: number) { this.buf.writeUInt16BE(v, this.offset); this.offset += 2; }
191
+ writeUint8(v: number) { this.buf.writeUInt8(v, this.offset); this.offset += 1; }
192
+ writeDomainName(nm: string) { for (const label of nm.split(".")) { if (label.length === 0) continue; this.writeUint8(label.length); Buffer.from(label).copy(this.buf, this.offset); this.offset += label.length; } this.writeUint8(0); }
193
+ finish(): Buffer { return this.buf.subarray(0, this.offset); }
194
+ })();
195
+
196
+ const testId = 0xDEAD;
197
+ testEncoder.writeUint16(testId);
198
+ testEncoder.writeUint16(0x0100);
199
+ testEncoder.writeUint16(1);
200
+ testEncoder.writeUint16(0);
201
+ testEncoder.writeUint16(0);
202
+ testEncoder.writeUint16(0);
203
+ testEncoder.writeDomainName("id.srv.loop");
204
+ testEncoder.writeUint16(DNS_TYPES.SRV);
205
+ testEncoder.writeUint16(1);
206
+
207
+ server = new DevDnsServer(config);
208
+ const response = server.resolve(testEncoder.finish())!;
209
+ assert.strictEqual(response.readUInt16BE(0), testId);
210
+ });
211
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "assert";
3
+ import * as net from "net";
4
+
5
+ let PORT_BASE = 61500;
6
+
7
+ function nextPort(): number {
8
+ return PORT_BASE++;
9
+ }
10
+
11
+ describe("DnsHandler - TCP Connection Limiting", () => {
12
+ it("rejects new connections after max is reached", async () => {
13
+ const port = nextPort();
14
+ const { DnsHandler } = await import("./dns-handler.js");
15
+ const { DevDnsServer } = await import("./dns-service.js");
16
+ const config: any = { port, hosts: {}, maxTcpConnections: 2, tcpIdleTimeoutMs: 30000, rateLimitMaxRequests: 0 };
17
+
18
+ const dnsServer = new DevDnsServer(config);
19
+ const handler = new DnsHandler(dnsServer, config);
20
+ await handler.start();
21
+
22
+ try {
23
+ await new Promise<void>((r) => setTimeout(r, 150));
24
+
25
+ const sockets: net.Socket[] = [];
26
+ for (let i = 0; i < 2; i++) {
27
+ const s = net.createConnection(port, "127.0.0.1", () => {});
28
+ sockets.push(s);
29
+ }
30
+
31
+ await new Promise<void>((r) => setTimeout(r, 150));
32
+
33
+ const thirdPromise = new Promise<boolean>((resolve) => {
34
+ const socket = net.createConnection(port, "127.0.0.1");
35
+ socket.on("connect", () => {
36
+ setTimeout(() => resolve(socket.destroyed), 500);
37
+ });
38
+ socket.on("error", () => resolve(true));
39
+ });
40
+
41
+ const destroyed = await thirdPromise;
42
+ assert.strictEqual(destroyed, true);
43
+
44
+ for (const s of sockets) s.destroy();
45
+ } finally {
46
+ await handler.stop();
47
+ }
48
+ });
49
+
50
+ it("admits connections when one closes (below max)", async () => {
51
+ const port = nextPort();
52
+ const { DnsHandler } = await import("./dns-handler.js");
53
+ const { DevDnsServer } = await import("./dns-service.js");
54
+ const config: any = { port, hosts: {}, maxTcpConnections: 2, tcpIdleTimeoutMs: 1000, rateLimitMaxRequests: 0 };
55
+
56
+ const dnsServer = new DevDnsServer(config);
57
+ const handler = new DnsHandler(dnsServer, config);
58
+ await handler.start();
59
+
60
+ try {
61
+ await new Promise<void>((r) => setTimeout(r, 150));
62
+
63
+ const socket1 = net.createConnection(port, "127.0.0.1");
64
+ const socket2 = net.createConnection(port, "127.0.0.1");
65
+
66
+ await new Promise<void>((r) => setTimeout(r, 150));
67
+
68
+ socket1.end();
69
+ await new Promise<void>((r) => { socket1.on("close", r); });
70
+
71
+ await new Promise<void>((r) => setTimeout(r, 200));
72
+
73
+ const thirdPromise = new Promise<boolean>((resolve) => {
74
+ const socket3 = net.createConnection(port, "127.0.0.1", () => resolve(true));
75
+ socket3.on("error", () => resolve(false));
76
+ socket3.setTimeout(500);
77
+ socket3.on("timeout", () => { socket3.destroy(); resolve(false); });
78
+ });
79
+
80
+ const admitted = await thirdPromise;
81
+ assert.strictEqual(admitted, true);
82
+ } finally {
83
+ await handler.stop();
84
+ }
85
+ });
86
+
87
+ it("closes idle connections after timeout", async () => {
88
+ const port = nextPort();
89
+ const { DnsHandler } = await import("./dns-handler.js");
90
+ const { DevDnsServer } = await import("./dns-service.js");
91
+ const config: any = { port, hosts: {}, maxTcpConnections: 5, tcpIdleTimeoutMs: 300, rateLimitMaxRequests: 0 };
92
+
93
+ const dnsServer = new DevDnsServer(config);
94
+ const handler = new DnsHandler(dnsServer, config);
95
+ await handler.start();
96
+
97
+ try {
98
+ await new Promise<void>((r) => setTimeout(r, 150));
99
+
100
+ const socket = net.createConnection(port, "127.0.0.1");
101
+ await new Promise<void>((r) => socket.on("connect", () => r()));
102
+
103
+ await new Promise<void>((r) => setTimeout(r, 800));
104
+
105
+ assert.strictEqual(socket.destroyed, true);
106
+ } finally {
107
+ await handler.stop();
108
+ }
109
+ });
110
+ });