@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/types.ts ADDED
@@ -0,0 +1,168 @@
1
+ export interface ARecord {
2
+ type: "A";
3
+ address: string;
4
+ }
5
+
6
+ export interface AAAARecord {
7
+ type: "AAAA";
8
+ address: string;
9
+ }
10
+
11
+ export interface CNAME {
12
+ type: "CNAME";
13
+ target: string;
14
+ }
15
+
16
+ export interface TXT {
17
+ type: "TXT";
18
+ data: string[];
19
+ }
20
+
21
+ export interface MX {
22
+ type: "MX";
23
+ priority: number;
24
+ exchange: string;
25
+ }
26
+
27
+ export interface NS {
28
+ type: "NS";
29
+ target: string;
30
+ }
31
+
32
+ export interface SRV {
33
+ type: "SRV";
34
+ priority: number;
35
+ weight: number;
36
+ port: number;
37
+ target: string;
38
+ }
39
+
40
+ export interface PTR {
41
+ type: "PTR";
42
+ target: string;
43
+ }
44
+
45
+ export type DnsRecord = ARecord | AAAARecord | CNAME | TXT | MX | NS | SRV | PTR;
46
+
47
+ export interface HttpProxyConfig {
48
+ enabled: boolean;
49
+ upstream?: string;
50
+ headers: Record<string, string>;
51
+ forwardRequestBody?: boolean;
52
+ maxRequestBodyBytes?: number;
53
+ }
54
+
55
+ export interface RedirectConfig {
56
+ enabled: boolean;
57
+ code: number;
58
+ target: string;
59
+ }
60
+
61
+ export interface HostConfig {
62
+ records: DnsRecord[];
63
+ http_proxy?: HttpProxyConfig;
64
+ redirect?: RedirectConfig;
65
+ }
66
+
67
+ export interface FirewallConfig {
68
+ defaultPolicy: "allow" | "deny";
69
+ allowlist_domains?: string[];
70
+ blocklist_domains?: string[];
71
+ allowlist_ranges?: string[];
72
+ blocklist_ranges?: string[];
73
+ allowlist_ips?: string[];
74
+ blocklist_ips?: string[];
75
+ }
76
+
77
+ export interface ControlPlaneConfig {
78
+ enabled?: boolean;
79
+ port?: number;
80
+ apiKey?: string;
81
+ }
82
+
83
+ export interface ServerConfig {
84
+ port: number;
85
+ fallbackDns?: string;
86
+ firewall?: FirewallConfig;
87
+ controlPlane?: ControlPlaneConfig;
88
+ dnsCacheMaxSize?: number;
89
+ dnsCacheTtlMs?: number;
90
+ maxTcpConnections?: number;
91
+ tcpIdleTimeoutMs?: number;
92
+ rateLimitMaxRequests?: number;
93
+ rateLimitWindowMs?: number;
94
+ hosts: Record<string, HostConfig>;
95
+ }
96
+
97
+ export interface DnsQuestion {
98
+ name: string;
99
+ type: number;
100
+ class: number;
101
+ }
102
+
103
+ export interface DnsResponseHeader {
104
+ id: number;
105
+ flags: number;
106
+ qdcount: number;
107
+ ancount: number;
108
+ nscount: number;
109
+ arcount: number;
110
+ }
111
+
112
+ export interface DnsQuestionSection {
113
+ questions: DnsQuestion[];
114
+ additional?: string;
115
+ }
116
+
117
+ export interface ProxiedRequest {
118
+ hostname: string;
119
+ originalUrl: string;
120
+ headers: Record<string, string>;
121
+ method: string;
122
+ body?: Buffer;
123
+ }
124
+
125
+ export interface ModifiedHeaders {
126
+ upstreamHeaders: Record<string, string>;
127
+ clientResponseHeaders: Record<string, string>;
128
+ }
129
+
130
+ export const DNS_CLASSES = { IN: 1 };
131
+
132
+ export const DNS_TYPES = {
133
+ A: 1,
134
+ NS: 2,
135
+ CNAME: 5,
136
+ SOA: 6,
137
+ MX: 15,
138
+ TXT: 16,
139
+ PTR: 12,
140
+ AAAA: 28,
141
+ SRV: 33,
142
+ } as const;
143
+
144
+ export const DNS_OPCODE = {
145
+ QUERY: 0,
146
+ IQUERY: 1,
147
+ STATUS: 2,
148
+ };
149
+
150
+ export const DNS_RCODE = {
151
+ NOERROR: 0,
152
+ FORMERR: 1,
153
+ SERVFAIL: 2,
154
+ NXDOMAIN: 3,
155
+ NOTIMP: 4,
156
+ REFUSED: 5,
157
+ };
158
+
159
+ export const RESPONSE_FLAGS = {
160
+ QR: (1 << 15) as number,
161
+ AA: (1 << 10) as number,
162
+ TC: (1 << 9) as number,
163
+ RD: (1 << 8) as number,
164
+ RA: (1 << 7) as number,
165
+ };
166
+
167
+ export const MAX_DNS_PACKET_SIZE = 512;
168
+ export const MAX_TXT_RECORD_LENGTH = 255;
@@ -0,0 +1,196 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "assert";
3
+ import { DevDnsServer } from "./dns-service.js";
4
+ import { ServerConfig, DNS_TYPES } 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(0xBEEF);
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 getAnswerCount(buf: Buffer): number {
51
+ return buf.readUInt16BE(6);
52
+ }
53
+
54
+ describe("DevDnsServer - Wildcard Host Matching", () => {
55
+ let server: DevDnsServer;
56
+
57
+ it("resolves subdomains under a wildcard pattern *.loop", () => {
58
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
59
+ port: 53,
60
+ dnsCacheMaxSize: 100,
61
+ dnsCacheTtlMs: 0,
62
+ hosts: {
63
+ "*.loop": { records: [{ type: "A", address: "192.168.1.1" }] },
64
+ },
65
+ };
66
+
67
+ server = new DevDnsServer(config);
68
+
69
+ const appResp = server.resolve(buildQuery("app.loop", DNS_TYPES.A))!;
70
+ assert.ok(appResp.length > 0);
71
+ assert.strictEqual(getAnswerCount(appResp), 1);
72
+
73
+ const deepResp = server.resolve(buildQuery("deep.sub.loop", DNS_TYPES.A))!;
74
+ assert.ok(deepResp.length > 0);
75
+ assert.strictEqual(getAnswerCount(deepResp), 1);
76
+ });
77
+
78
+ it("resolves wildcard with multiple record types", () => {
79
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
80
+ port: 53,
81
+ dnsCacheMaxSize: 100,
82
+ dnsCacheTtlMs: 0,
83
+ hosts: {
84
+ "*.dev": {
85
+ records: [
86
+ { type: "A", address: "10.0.0.1" },
87
+ { type: "AAAA", address: "::1" },
88
+ { type: "TXT", data: ["v=dev"] },
89
+ ],
90
+ },
91
+ },
92
+ };
93
+
94
+ server = new DevDnsServer(config);
95
+
96
+ const aResp = server.resolve(buildQuery("random.dev", DNS_TYPES.A))!;
97
+ assert.ok(aResp.length > 0);
98
+ assert.strictEqual(getAnswerCount(aResp), 1);
99
+
100
+ const aaaaResp = server.resolve(buildQuery("another.dev", DNS_TYPES.AAAA))!;
101
+ assert.ok(aaaaResp.length > 0);
102
+ assert.strictEqual(getAnswerCount(aaaaResp), 1);
103
+
104
+ const txtResp = server.resolve(buildQuery("foo.dev", DNS_TYPES.TXT))!;
105
+ assert.ok(txtResp.length > 0);
106
+ assert.strictEqual(getAnswerCount(txtResp), 1);
107
+ });
108
+
109
+ it("exact hostname takes priority over wildcard", () => {
110
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
111
+ port: 53,
112
+ dnsCacheMaxSize: 100,
113
+ dnsCacheTtlMs: 0,
114
+ hosts: {
115
+ "*.loop": { records: [{ type: "A", address: "192.168.1.1" }] },
116
+ "exact.loop": { records: [{ type: "A", address: "10.0.0.100" }] },
117
+ },
118
+ };
119
+
120
+ server = new DevDnsServer(config);
121
+
122
+ const exactResp = server.resolve(buildQuery("exact.loop", DNS_TYPES.A))!;
123
+ assert.ok(exactResp.length > 0);
124
+ assert.strictEqual(getAnswerCount(exactResp), 1);
125
+
126
+ const wildResp = server.resolve(buildQuery("other.loop", DNS_TYPES.A))!;
127
+ assert.ok(wildResp.length > 0);
128
+ assert.strictEqual(getAnswerCount(wildResp), 1);
129
+ });
130
+
131
+ it("rejects bare wildcard pattern query (no subdomain)", () => {
132
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
133
+ port: 53,
134
+ dnsCacheMaxSize: 100,
135
+ dnsCacheTtlMs: 0,
136
+ hosts: {
137
+ "*.loop": { records: [{ type: "A", address: "192.168.1.1" }] },
138
+ },
139
+ };
140
+
141
+ server = new DevDnsServer(config);
142
+
143
+ const bareResp = server.resolve(buildQuery("*.loop", DNS_TYPES.A))!;
144
+ assert.ok(bareResp.length > 0);
145
+ });
146
+
147
+ it("handles deeply nested wildcard matches", () => {
148
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
149
+ port: 53,
150
+ dnsCacheMaxSize: 100,
151
+ dnsCacheTtlMs: 0,
152
+ hosts: {
153
+ "*.com": { records: [{ type: "A", address: "8.8.8.8" }] },
154
+ },
155
+ };
156
+
157
+ server = new DevDnsServer(config);
158
+
159
+ const deepResp = server.resolve(buildQuery("a.b.c.d.com", DNS_TYPES.A))!;
160
+ assert.ok(deepResp.length > 0);
161
+ assert.strictEqual(getAnswerCount(deepResp), 1);
162
+ });
163
+
164
+ it("is case-insensitive for wildcard matching", () => {
165
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
166
+ port: 53,
167
+ dnsCacheMaxSize: 100,
168
+ dnsCacheTtlMs: 0,
169
+ hosts: {
170
+ "*.example": { records: [{ type: "A", address: "1.2.3.4" }] },
171
+ },
172
+ };
173
+
174
+ server = new DevDnsServer(config);
175
+
176
+ const upperResp = server.resolve(buildQuery("APP.EXAMPLE", DNS_TYPES.A))!;
177
+ assert.ok(upperResp.length > 0);
178
+ assert.strictEqual(getAnswerCount(upperResp), 1);
179
+ });
180
+
181
+ it("wildcard only matches one label prefix", () => {
182
+ const config: ServerConfig & { dnsCacheMaxSize?: number; dnsCacheTtlMs?: number } = {
183
+ port: 53,
184
+ dnsCacheMaxSize: 100,
185
+ dnsCacheTtlMs: 0,
186
+ hosts: {
187
+ "*.loop": { records: [{ type: "A", address: "5.5.5.5" }] },
188
+ },
189
+ };
190
+
191
+ server = new DevDnsServer(config);
192
+
193
+ const oneLabelResp = server.resolve(buildQuery("x.loop", DNS_TYPES.A))!;
194
+ assert.ok(oneLabelResp.length > 0);
195
+ });
196
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*.ts", "src/env.d.ts"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }