dns-security-mcp 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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +723 -0
  3. package/dist/blocklist/index.d.ts +3 -0
  4. package/dist/blocklist/index.d.ts.map +1 -0
  5. package/dist/blocklist/index.js +596 -0
  6. package/dist/blocklist/index.js.map +1 -0
  7. package/dist/ct/index.d.ts +3 -0
  8. package/dist/ct/index.d.ts.map +1 -0
  9. package/dist/ct/index.js +534 -0
  10. package/dist/ct/index.js.map +1 -0
  11. package/dist/data/dkim-selectors.d.ts +2 -0
  12. package/dist/data/dkim-selectors.d.ts.map +1 -0
  13. package/dist/data/dkim-selectors.js +60 -0
  14. package/dist/data/dkim-selectors.js.map +1 -0
  15. package/dist/data/dnsbl-lists.d.ts +8 -0
  16. package/dist/data/dnsbl-lists.d.ts.map +1 -0
  17. package/dist/data/dnsbl-lists.js +54 -0
  18. package/dist/data/dnsbl-lists.js.map +1 -0
  19. package/dist/data/takeover-fingerprints.d.ts +8 -0
  20. package/dist/data/takeover-fingerprints.d.ts.map +1 -0
  21. package/dist/data/takeover-fingerprints.js +84 -0
  22. package/dist/data/takeover-fingerprints.js.map +1 -0
  23. package/dist/data/tunneling-signatures.d.ts +17 -0
  24. package/dist/data/tunneling-signatures.d.ts.map +1 -0
  25. package/dist/data/tunneling-signatures.js +85 -0
  26. package/dist/data/tunneling-signatures.js.map +1 -0
  27. package/dist/dns/index.d.ts +3 -0
  28. package/dist/dns/index.d.ts.map +1 -0
  29. package/dist/dns/index.js +1211 -0
  30. package/dist/dns/index.js.map +1 -0
  31. package/dist/dnssec/index.d.ts +3 -0
  32. package/dist/dnssec/index.d.ts.map +1 -0
  33. package/dist/dnssec/index.js +1377 -0
  34. package/dist/dnssec/index.js.map +1 -0
  35. package/dist/domain/index.d.ts +3 -0
  36. package/dist/domain/index.d.ts.map +1 -0
  37. package/dist/domain/index.js +938 -0
  38. package/dist/domain/index.js.map +1 -0
  39. package/dist/email/index.d.ts +3 -0
  40. package/dist/email/index.d.ts.map +1 -0
  41. package/dist/email/index.js +1188 -0
  42. package/dist/email/index.js.map +1 -0
  43. package/dist/hijack/index.d.ts +3 -0
  44. package/dist/hijack/index.d.ts.map +1 -0
  45. package/dist/hijack/index.js +1117 -0
  46. package/dist/hijack/index.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +151 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/infra/index.d.ts +3 -0
  52. package/dist/infra/index.d.ts.map +1 -0
  53. package/dist/infra/index.js +797 -0
  54. package/dist/infra/index.js.map +1 -0
  55. package/dist/privacy/index.d.ts +3 -0
  56. package/dist/privacy/index.d.ts.map +1 -0
  57. package/dist/privacy/index.js +772 -0
  58. package/dist/privacy/index.js.map +1 -0
  59. package/dist/protocol/mcp-server.d.ts +4 -0
  60. package/dist/protocol/mcp-server.d.ts.map +1 -0
  61. package/dist/protocol/mcp-server.js +32 -0
  62. package/dist/protocol/mcp-server.js.map +1 -0
  63. package/dist/protocol/tools.d.ts +3 -0
  64. package/dist/protocol/tools.d.ts.map +1 -0
  65. package/dist/protocol/tools.js +29 -0
  66. package/dist/protocol/tools.js.map +1 -0
  67. package/dist/report/index.d.ts +3 -0
  68. package/dist/report/index.d.ts.map +1 -0
  69. package/dist/report/index.js +1167 -0
  70. package/dist/report/index.js.map +1 -0
  71. package/dist/threat/index.d.ts +3 -0
  72. package/dist/threat/index.d.ts.map +1 -0
  73. package/dist/threat/index.js +999 -0
  74. package/dist/threat/index.js.map +1 -0
  75. package/dist/tunnel/index.d.ts +3 -0
  76. package/dist/tunnel/index.d.ts.map +1 -0
  77. package/dist/tunnel/index.js +688 -0
  78. package/dist/tunnel/index.js.map +1 -0
  79. package/dist/types/index.d.ts +52 -0
  80. package/dist/types/index.d.ts.map +1 -0
  81. package/dist/types/index.js +8 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/typo/index.d.ts +3 -0
  84. package/dist/typo/index.d.ts.map +1 -0
  85. package/dist/typo/index.js +625 -0
  86. package/dist/typo/index.js.map +1 -0
  87. package/dist/utils/cache.d.ts +11 -0
  88. package/dist/utils/cache.d.ts.map +1 -0
  89. package/dist/utils/cache.js +35 -0
  90. package/dist/utils/cache.js.map +1 -0
  91. package/dist/utils/dns-client.d.ts +37 -0
  92. package/dist/utils/dns-client.d.ts.map +1 -0
  93. package/dist/utils/dns-client.js +359 -0
  94. package/dist/utils/dns-client.js.map +1 -0
  95. package/dist/utils/rate-limiter.d.ts +10 -0
  96. package/dist/utils/rate-limiter.d.ts.map +1 -0
  97. package/dist/utils/rate-limiter.js +35 -0
  98. package/dist/utils/rate-limiter.js.map +1 -0
  99. package/package.json +63 -0
@@ -0,0 +1,772 @@
1
+ import { z } from "zod";
2
+ import { json } from "../types/index.js";
3
+ import { queryDoH, queryDoT, queryRaw } from "../utils/dns-client.js";
4
+ import * as tls from "node:tls";
5
+ import * as dgram from "node:dgram";
6
+ import * as dns from "node:dns/promises";
7
+ import * as dnsPacket from "dns-packet";
8
+ // ─── Constants ───
9
+ const DEFAULT_DOH_SERVERS = {
10
+ "1.1.1.1": "https://cloudflare-dns.com/dns-query",
11
+ "cloudflare": "https://cloudflare-dns.com/dns-query",
12
+ "8.8.8.8": "https://dns.google/dns-query",
13
+ "google": "https://dns.google/dns-query",
14
+ "dns.google": "https://dns.google/dns-query",
15
+ "9.9.9.9": "https://dns.quad9.net/dns-query",
16
+ "quad9": "https://dns.quad9.net/dns-query",
17
+ };
18
+ function resolveDoHUrl(server) {
19
+ const lower = server.toLowerCase().trim();
20
+ if (DEFAULT_DOH_SERVERS[lower])
21
+ return DEFAULT_DOH_SERVERS[lower];
22
+ if (server.startsWith("https://"))
23
+ return server;
24
+ return `https://${server}/dns-query`;
25
+ }
26
+ function resolveDoTHost(server) {
27
+ const lower = server.toLowerCase().trim();
28
+ if (lower === "1.1.1.1" || lower === "cloudflare")
29
+ return "1.1.1.1";
30
+ if (lower === "8.8.8.8" || lower === "google" || lower === "dns.google")
31
+ return "dns.google";
32
+ if (lower === "9.9.9.9" || lower === "quad9")
33
+ return "dns.quad9.net";
34
+ return server;
35
+ }
36
+ // ─── Tool 1: privacy_doh_test ───
37
+ const privacyDohTest = {
38
+ name: "privacy_doh_test",
39
+ description: "Test DNS-over-HTTPS (DoH) endpoint connectivity and response. Sends a JSON API query and checks response format, status, and DNSSEC (AD bit) support. Supports Cloudflare, Google, Quad9 and custom servers.",
40
+ schema: {
41
+ server: z
42
+ .string()
43
+ .describe("DoH server address or name (e.g. '1.1.1.1', 'cloudflare', 'dns.google', or full URL 'https://dns.example.com/dns-query')"),
44
+ },
45
+ async execute(args) {
46
+ const server = args.server;
47
+ const dohUrl = resolveDoHUrl(server);
48
+ try {
49
+ const startTime = Date.now();
50
+ const result = await queryDoH("example.com", "A", dohUrl);
51
+ const latency = Date.now() - startTime;
52
+ const hasAnswers = result.answers && result.answers.length > 0;
53
+ // Test DNSSEC by querying a signed domain
54
+ let dnssecSupported = false;
55
+ try {
56
+ const dnssecUrl = `${dohUrl}?name=example.com&type=A&do=1`;
57
+ const dnssecRes = await fetch(dnssecUrl, {
58
+ headers: { Accept: "application/dns-json" },
59
+ signal: AbortSignal.timeout(5000),
60
+ });
61
+ if (dnssecRes.ok) {
62
+ const dnssecData = (await dnssecRes.json());
63
+ dnssecSupported = dnssecData.AD === true;
64
+ }
65
+ }
66
+ catch {
67
+ // DNSSEC check failed
68
+ }
69
+ const findings = [];
70
+ findings.push(`OK: DoH endpoint is reachable at ${dohUrl}`);
71
+ findings.push(`Latency: ${latency}ms`);
72
+ if (hasAnswers) {
73
+ findings.push(`Response contains ${result.answers.length} answer(s)`);
74
+ }
75
+ else {
76
+ findings.push("WARNING: No answers in response");
77
+ }
78
+ if (dnssecSupported) {
79
+ findings.push("OK: DNSSEC validation supported (AD bit set)");
80
+ }
81
+ else {
82
+ findings.push("INFO: DNSSEC AD bit not set in DoH response");
83
+ }
84
+ return json({
85
+ server,
86
+ doh_url: dohUrl,
87
+ reachable: true,
88
+ latency_ms: latency,
89
+ answers_count: result.answers?.length ?? 0,
90
+ dnssec_ad_bit: dnssecSupported,
91
+ sample_answers: result.answers?.slice(0, 5) ?? [],
92
+ findings,
93
+ });
94
+ }
95
+ catch (err) {
96
+ return json({
97
+ server,
98
+ doh_url: dohUrl,
99
+ reachable: false,
100
+ error: err.message,
101
+ findings: [`FAIL: DoH endpoint unreachable at ${dohUrl}: ${err.message}`],
102
+ });
103
+ }
104
+ },
105
+ };
106
+ // ─── Tool 2: privacy_dot_test ───
107
+ const privacyDotTest = {
108
+ name: "privacy_dot_test",
109
+ description: "Test DNS-over-TLS (DoT) endpoint connectivity and security. Reports TLS version, cipher suite, certificate validity, and DNS response. Validates that encrypted DNS transport is properly configured.",
110
+ schema: {
111
+ server: z
112
+ .string()
113
+ .describe("DoT server address or name (e.g. '1.1.1.1', 'dns.google', 'quad9')"),
114
+ port: z
115
+ .number()
116
+ .optional()
117
+ .describe("DoT port number (default: 853)"),
118
+ },
119
+ async execute(args) {
120
+ const server = args.server;
121
+ const port = args.port ?? 853;
122
+ const dotHost = resolveDoTHost(server);
123
+ // First, probe TLS details
124
+ const tlsInfo = await new Promise((resolve) => {
125
+ const socket = tls.connect(port, dotHost, { servername: dotHost }, () => {
126
+ const cipher = socket.getCipher();
127
+ const cert = socket.getPeerCertificate();
128
+ const authorized = socket.authorized;
129
+ resolve({
130
+ connected: true,
131
+ tls_version: socket.getProtocol?.() ?? cipher?.version ?? null,
132
+ cipher: cipher?.name ?? null,
133
+ cert_valid: authorized,
134
+ cert_issuer: cert?.issuer ? Object.values(cert.issuer).join(", ") : null,
135
+ cert_subject: (Array.isArray(cert?.subject?.CN) ? cert.subject.CN[0] : cert?.subject?.CN) ?? null,
136
+ cert_expires: cert?.valid_to ?? null,
137
+ error: null,
138
+ });
139
+ socket.destroy();
140
+ });
141
+ const timer = setTimeout(() => {
142
+ socket.destroy();
143
+ resolve({
144
+ connected: false,
145
+ tls_version: null,
146
+ cipher: null,
147
+ cert_valid: false,
148
+ cert_issuer: null,
149
+ cert_subject: null,
150
+ cert_expires: null,
151
+ error: "Connection timeout",
152
+ });
153
+ }, 5000);
154
+ socket.on("error", (err) => {
155
+ clearTimeout(timer);
156
+ resolve({
157
+ connected: false,
158
+ tls_version: null,
159
+ cipher: null,
160
+ cert_valid: false,
161
+ cert_issuer: null,
162
+ cert_subject: null,
163
+ cert_expires: null,
164
+ error: err.message,
165
+ });
166
+ });
167
+ socket.on("secureConnect", () => {
168
+ clearTimeout(timer);
169
+ });
170
+ });
171
+ // Then test actual DNS query over DoT
172
+ let dnsResponseValid = false;
173
+ let latency = null;
174
+ let answersCount = 0;
175
+ if (tlsInfo.connected) {
176
+ try {
177
+ const startTime = Date.now();
178
+ const answers = await queryDoT("example.com", "A", dotHost, port);
179
+ latency = Date.now() - startTime;
180
+ dnsResponseValid = answers.length > 0;
181
+ answersCount = answers.length;
182
+ }
183
+ catch {
184
+ dnsResponseValid = false;
185
+ }
186
+ }
187
+ const findings = [];
188
+ if (!tlsInfo.connected) {
189
+ findings.push(`FAIL: Could not connect to DoT server ${dotHost}:${port}`);
190
+ if (tlsInfo.error)
191
+ findings.push(`Error: ${tlsInfo.error}`);
192
+ }
193
+ else {
194
+ findings.push(`OK: DoT server reachable at ${dotHost}:${port}`);
195
+ if (tlsInfo.tls_version) {
196
+ const ver = tlsInfo.tls_version;
197
+ if (ver === "TLSv1.3") {
198
+ findings.push("OK: TLS 1.3 — optimal security");
199
+ }
200
+ else if (ver === "TLSv1.2") {
201
+ findings.push("OK: TLS 1.2 — acceptable security");
202
+ }
203
+ else {
204
+ findings.push(`WARNING: ${ver} — outdated TLS version, recommend TLS 1.2+`);
205
+ }
206
+ }
207
+ if (tlsInfo.cipher) {
208
+ findings.push(`Cipher: ${tlsInfo.cipher}`);
209
+ }
210
+ if (!tlsInfo.cert_valid) {
211
+ findings.push("WARNING: TLS certificate validation failed");
212
+ }
213
+ else {
214
+ findings.push("OK: TLS certificate is valid");
215
+ }
216
+ if (dnsResponseValid) {
217
+ findings.push(`OK: DNS query succeeded over DoT (${answersCount} answer(s), ${latency}ms)`);
218
+ }
219
+ else {
220
+ findings.push("WARNING: TLS connected but DNS query failed");
221
+ }
222
+ }
223
+ return json({
224
+ server,
225
+ dot_host: dotHost,
226
+ port,
227
+ connected: tlsInfo.connected,
228
+ tls_version: tlsInfo.tls_version,
229
+ cipher: tlsInfo.cipher,
230
+ cert_valid: tlsInfo.cert_valid,
231
+ cert_issuer: tlsInfo.cert_issuer,
232
+ cert_subject: tlsInfo.cert_subject,
233
+ cert_expires: tlsInfo.cert_expires,
234
+ dns_response_valid: dnsResponseValid,
235
+ dns_latency_ms: latency,
236
+ answers_count: answersCount,
237
+ findings,
238
+ });
239
+ },
240
+ };
241
+ // ─── Tool 3: privacy_doq_test ───
242
+ const privacyDoqTest = {
243
+ name: "privacy_doq_test",
244
+ description: "Test DNS-over-QUIC (DoQ) support on a server. Since Node.js QUIC support is experimental, performs a connectivity probe by sending a DNS query to port 853/UDP and checking for any response. Reports availability status.",
245
+ schema: {
246
+ server: z
247
+ .string()
248
+ .describe("Server address to test DoQ support (e.g. 'dns.adguard-dns.com', '94.140.14.14')"),
249
+ },
250
+ async execute(args) {
251
+ const server = args.server;
252
+ // DoQ uses port 853 UDP (same as DoT uses TCP, but QUIC is UDP-based)
253
+ const doqPort = 853;
254
+ // Build a DNS query packet to send over UDP to port 853
255
+ const queryBuf = dnsPacket.encode({
256
+ type: "query",
257
+ id: Math.floor(Math.random() * 65535),
258
+ flags: dnsPacket.RECURSION_DESIRED,
259
+ questions: [{ type: "A", name: "example.com", class: "IN" }],
260
+ });
261
+ // DoQ uses QUIC framing, so a raw UDP DNS packet won't get a proper response,
262
+ // but we can detect if the port is open/responding at all
263
+ const result = await new Promise((resolve) => {
264
+ const socket = dgram.createSocket("udp4");
265
+ let responded = false;
266
+ const timer = setTimeout(() => {
267
+ socket.close();
268
+ if (!responded) {
269
+ resolve({
270
+ port_open: false,
271
+ received_response: false,
272
+ response_bytes: 0,
273
+ error: "No response within timeout (port may be filtered or DoQ not supported)",
274
+ });
275
+ }
276
+ }, 5000);
277
+ socket.on("message", (msg) => {
278
+ responded = true;
279
+ clearTimeout(timer);
280
+ socket.close();
281
+ resolve({
282
+ port_open: true,
283
+ received_response: true,
284
+ response_bytes: msg.length,
285
+ error: null,
286
+ });
287
+ });
288
+ socket.on("error", (err) => {
289
+ responded = true;
290
+ clearTimeout(timer);
291
+ socket.close();
292
+ resolve({
293
+ port_open: false,
294
+ received_response: false,
295
+ response_bytes: 0,
296
+ error: err.message,
297
+ });
298
+ });
299
+ socket.send(queryBuf, 0, queryBuf.length, doqPort, server);
300
+ });
301
+ // Also check if we can detect DoQ via a known-good DoQ resolver list
302
+ const knownDoqServers = [
303
+ "dns.adguard-dns.com",
304
+ "dns.adguard.com",
305
+ "unfiltered.adguard-dns.com",
306
+ "dns.nextdns.io",
307
+ ];
308
+ const isKnownDoq = knownDoqServers.some((s) => server.toLowerCase().includes(s) || s.includes(server.toLowerCase()));
309
+ const findings = [];
310
+ if (result.received_response) {
311
+ findings.push(`OK: Port ${doqPort}/UDP responded (${result.response_bytes} bytes)`);
312
+ findings.push("Note: Response may be a QUIC version negotiation or ICMP unreachable — further validation needed");
313
+ }
314
+ else if (result.error?.includes("ICMP") || result.error?.includes("ECONNREFUSED")) {
315
+ findings.push(`FAIL: Port ${doqPort}/UDP actively refused connection`);
316
+ }
317
+ else {
318
+ findings.push(`INCONCLUSIVE: No response from ${server}:${doqPort}/UDP within timeout`);
319
+ findings.push("Note: This may mean DoQ is not supported, or a firewall is filtering UDP 853");
320
+ }
321
+ if (isKnownDoq) {
322
+ findings.push(`INFO: ${server} is a known DoQ-capable resolver (based on known server list)`);
323
+ }
324
+ findings.push("Note: Full DoQ validation requires QUIC protocol support (experimental in Node.js)");
325
+ findings.push("Recommendation: Use tools like 'q' (DNS client) or 'kdig' for full DoQ testing");
326
+ return json({
327
+ server,
328
+ port: doqPort,
329
+ protocol: "QUIC/UDP",
330
+ port_responded: result.received_response,
331
+ response_bytes: result.response_bytes,
332
+ known_doq_server: isKnownDoq,
333
+ error: result.error,
334
+ findings,
335
+ });
336
+ },
337
+ };
338
+ // ─── Tool 4: privacy_ecs_leak ───
339
+ const privacyEcsLeak = {
340
+ name: "privacy_ecs_leak",
341
+ description: "Test EDNS Client Subnet (ECS) leak on a DNS resolver. Sends a query with an ECS option containing a /24 subnet to check if the resolver forwards client subnet information to authoritative servers, potentially exposing client location.",
342
+ schema: {
343
+ resolver: z
344
+ .string()
345
+ .describe("IP address of the DNS resolver to test for ECS forwarding"),
346
+ test_domain: z
347
+ .string()
348
+ .optional()
349
+ .describe("Test domain to use (default: 'o-o.myaddr.l.google.com' which echoes resolver info)"),
350
+ },
351
+ async execute(args) {
352
+ const resolver = args.resolver;
353
+ const testDomain = args.test_domain ?? "o-o.myaddr.l.google.com";
354
+ try {
355
+ // Build query with ECS option (option code 8 = CLIENT_SUBNET)
356
+ // ECS format: family (2 bytes) + source prefix length (1 byte) + scope prefix length (1 byte) + address
357
+ const ecsData = Buffer.alloc(8);
358
+ ecsData.writeUInt16BE(1, 0); // Address family: IPv4
359
+ ecsData.writeUInt8(24, 2); // Source prefix length: /24
360
+ ecsData.writeUInt8(0, 3); // Scope prefix length: 0 (set by server)
361
+ // Use a test subnet 198.51.100.0/24 (TEST-NET-2, RFC 5737)
362
+ ecsData.writeUInt8(198, 4);
363
+ ecsData.writeUInt8(51, 5);
364
+ ecsData.writeUInt8(100, 6);
365
+ // Only 3 bytes needed for /24
366
+ const buf = dnsPacket.encode({
367
+ type: "query",
368
+ id: Math.floor(Math.random() * 65535),
369
+ flags: dnsPacket.RECURSION_DESIRED,
370
+ questions: [{ type: "TXT", name: testDomain, class: "IN" }],
371
+ additionals: [
372
+ {
373
+ type: "OPT",
374
+ name: ".",
375
+ udpPayloadSize: 4096,
376
+ flags: 0,
377
+ options: [
378
+ {
379
+ code: 8, // CLIENT_SUBNET
380
+ data: ecsData.subarray(0, 7), // 4 header bytes + 3 address bytes
381
+ },
382
+ ],
383
+ },
384
+ ],
385
+ });
386
+ const response = await new Promise((resolve, reject) => {
387
+ const socket = dgram.createSocket("udp4");
388
+ const timer = setTimeout(() => {
389
+ socket.close();
390
+ reject(new Error("Timeout waiting for DNS response"));
391
+ }, 5000);
392
+ socket.on("message", (msg) => {
393
+ clearTimeout(timer);
394
+ socket.close();
395
+ resolve(msg);
396
+ });
397
+ socket.on("error", (err) => {
398
+ clearTimeout(timer);
399
+ socket.close();
400
+ reject(err);
401
+ });
402
+ socket.send(buf, 0, buf.length, 53, resolver);
403
+ });
404
+ const decoded = dnsPacket.decode(response);
405
+ // Check if ECS was echoed back in the response
406
+ let ecsInResponse = false;
407
+ let ecsScopePrefix = 0;
408
+ let ecsSourcePrefix = 0;
409
+ for (const add of decoded.additionals ?? []) {
410
+ if (add.type === "OPT") {
411
+ for (const opt of add.options ?? []) {
412
+ if (opt.code === 8) {
413
+ ecsInResponse = true;
414
+ const optData = opt.data;
415
+ if (optData.length >= 4) {
416
+ ecsSourcePrefix = optData.readUInt8(2);
417
+ ecsScopePrefix = optData.readUInt8(3);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+ // Check TXT answers for resolver IP info (from Google's o-o.myaddr service)
424
+ const txtAnswers = [];
425
+ for (const answer of decoded.answers ?? []) {
426
+ if (answer.type === "TXT") {
427
+ const data = answer.data;
428
+ if (Buffer.isBuffer(data)) {
429
+ txtAnswers.push(data.toString("utf-8"));
430
+ }
431
+ else if (Array.isArray(data)) {
432
+ txtAnswers.push(data.map((d) => Buffer.isBuffer(d) ? d.toString("utf-8") : String(d)).join(""));
433
+ }
434
+ else if (typeof data === "string") {
435
+ txtAnswers.push(data);
436
+ }
437
+ }
438
+ }
439
+ const findings = [];
440
+ if (ecsInResponse) {
441
+ findings.push("WARNING: EDNS Client Subnet (ECS) is forwarded by this resolver");
442
+ findings.push(`Source prefix: /${ecsSourcePrefix}, Scope prefix: /${ecsScopePrefix}`);
443
+ findings.push("Impact: Your approximate network location is shared with authoritative DNS servers");
444
+ findings.push("Privacy risk: Authoritative servers can correlate queries to geographic regions");
445
+ findings.push("Remediation: Use a resolver that strips ECS (e.g., Cloudflare 1.1.1.1, Quad9)");
446
+ }
447
+ else {
448
+ findings.push("OK: Resolver does not appear to forward EDNS Client Subnet information");
449
+ findings.push("Your network location is not exposed to authoritative DNS servers via ECS");
450
+ }
451
+ if (txtAnswers.length > 0) {
452
+ findings.push(`Resolver identity from test domain: ${txtAnswers.join(", ")}`);
453
+ }
454
+ return json({
455
+ resolver,
456
+ test_domain: testDomain,
457
+ ecs_forwarded: ecsInResponse,
458
+ ecs_source_prefix: ecsInResponse ? ecsSourcePrefix : null,
459
+ ecs_scope_prefix: ecsInResponse ? ecsScopePrefix : null,
460
+ resolver_identity: txtAnswers,
461
+ findings,
462
+ });
463
+ }
464
+ catch (err) {
465
+ return json({
466
+ resolver,
467
+ test_domain: testDomain,
468
+ ecs_forwarded: null,
469
+ error: err.message,
470
+ findings: [`Could not test ECS: ${err.message}`],
471
+ });
472
+ }
473
+ },
474
+ };
475
+ // ─── Tool 5: privacy_resolver_audit ───
476
+ const privacyResolverAudit = {
477
+ name: "privacy_resolver_audit",
478
+ description: "Comprehensive privacy audit of a DNS resolver. Tests DoH support, DoT support, DNSSEC validation, and DNS Cookie support. Returns a privacy score from 0-100 based on the combined results.",
479
+ schema: {
480
+ resolver: z
481
+ .string()
482
+ .describe("IP address or hostname of the DNS resolver to audit"),
483
+ },
484
+ async execute(args) {
485
+ const resolver = args.resolver;
486
+ let score = 0;
487
+ const maxScore = 100;
488
+ const checks = [];
489
+ // Check 1: DoH support (25 points)
490
+ let dohSupported = false;
491
+ try {
492
+ const dohUrl = resolveDoHUrl(resolver);
493
+ const dohResult = await queryDoH("example.com", "A", dohUrl);
494
+ dohSupported = (dohResult.answers?.length ?? 0) > 0;
495
+ }
496
+ catch {
497
+ // Try direct HTTPS
498
+ try {
499
+ const res = await fetch(`https://${resolver}/dns-query?name=example.com&type=A`, {
500
+ headers: { Accept: "application/dns-json" },
501
+ signal: AbortSignal.timeout(5000),
502
+ });
503
+ dohSupported = res.ok;
504
+ }
505
+ catch {
506
+ dohSupported = false;
507
+ }
508
+ }
509
+ checks.push({
510
+ name: "DNS-over-HTTPS (DoH)",
511
+ passed: dohSupported,
512
+ weight: 25,
513
+ detail: dohSupported ? "DoH endpoint accessible" : "DoH not available or unreachable",
514
+ });
515
+ if (dohSupported)
516
+ score += 25;
517
+ // Check 2: DoT support (25 points)
518
+ let dotSupported = false;
519
+ try {
520
+ const dotHost = resolveDoTHost(resolver);
521
+ await new Promise((resolve, reject) => {
522
+ const socket = tls.connect(853, dotHost, { servername: dotHost }, () => {
523
+ dotSupported = true;
524
+ socket.destroy();
525
+ resolve();
526
+ });
527
+ const timer = setTimeout(() => {
528
+ socket.destroy();
529
+ reject(new Error("timeout"));
530
+ }, 5000);
531
+ socket.on("error", (err) => {
532
+ clearTimeout(timer);
533
+ reject(err);
534
+ });
535
+ socket.on("secureConnect", () => {
536
+ clearTimeout(timer);
537
+ });
538
+ });
539
+ }
540
+ catch {
541
+ dotSupported = false;
542
+ }
543
+ checks.push({
544
+ name: "DNS-over-TLS (DoT)",
545
+ passed: dotSupported,
546
+ weight: 25,
547
+ detail: dotSupported ? "DoT port 853 accessible with valid TLS" : "DoT not available on port 853",
548
+ });
549
+ if (dotSupported)
550
+ score += 25;
551
+ // Check 3: DNSSEC validation (30 points)
552
+ let dnssecValidation = false;
553
+ try {
554
+ const result = await queryRaw("example.com", "A", resolver, 53, { rd: true, dnssec: true });
555
+ dnssecValidation = result.flags.authenticatedData;
556
+ }
557
+ catch {
558
+ dnssecValidation = false;
559
+ }
560
+ checks.push({
561
+ name: "DNSSEC Validation",
562
+ passed: dnssecValidation,
563
+ weight: 30,
564
+ detail: dnssecValidation ? "DNSSEC validation active (AD bit set)" : "DNSSEC validation not detected (AD bit not set)",
565
+ });
566
+ if (dnssecValidation)
567
+ score += 30;
568
+ // Check 4: DNS Cookie support (20 points)
569
+ let cookieSupported = false;
570
+ try {
571
+ const clientCookie = Buffer.alloc(8);
572
+ for (let i = 0; i < 8; i++)
573
+ clientCookie[i] = Math.floor(Math.random() * 256);
574
+ const cookieBuf = dnsPacket.encode({
575
+ type: "query",
576
+ id: Math.floor(Math.random() * 65535),
577
+ flags: dnsPacket.RECURSION_DESIRED,
578
+ questions: [{ type: "A", name: "example.com", class: "IN" }],
579
+ additionals: [
580
+ {
581
+ type: "OPT",
582
+ name: ".",
583
+ udpPayloadSize: 4096,
584
+ flags: 0,
585
+ options: [{ code: 10, data: clientCookie }],
586
+ },
587
+ ],
588
+ });
589
+ const response = await new Promise((resolve, reject) => {
590
+ const socket = dgram.createSocket("udp4");
591
+ const timer = setTimeout(() => {
592
+ socket.close();
593
+ reject(new Error("timeout"));
594
+ }, 5000);
595
+ socket.on("message", (msg) => {
596
+ clearTimeout(timer);
597
+ socket.close();
598
+ resolve(msg);
599
+ });
600
+ socket.on("error", (err) => {
601
+ clearTimeout(timer);
602
+ socket.close();
603
+ reject(err);
604
+ });
605
+ socket.send(cookieBuf, 0, cookieBuf.length, 53, resolver);
606
+ });
607
+ const decoded = dnsPacket.decode(response);
608
+ for (const add of decoded.additionals ?? []) {
609
+ if (add.type === "OPT") {
610
+ for (const opt of add.options ?? []) {
611
+ if (opt.code === 10 && opt.data.length > 8) {
612
+ cookieSupported = true;
613
+ }
614
+ }
615
+ }
616
+ }
617
+ }
618
+ catch {
619
+ cookieSupported = false;
620
+ }
621
+ checks.push({
622
+ name: "DNS Cookies (RFC 7873)",
623
+ passed: cookieSupported,
624
+ weight: 20,
625
+ detail: cookieSupported ? "DNS cookies supported (anti-spoofing protection)" : "DNS cookies not supported",
626
+ });
627
+ if (cookieSupported)
628
+ score += 20;
629
+ // Build findings
630
+ const findings = [];
631
+ findings.push(`Privacy Score: ${score}/${maxScore}`);
632
+ if (score >= 80) {
633
+ findings.push("EXCELLENT: This resolver has strong privacy protections");
634
+ }
635
+ else if (score >= 50) {
636
+ findings.push("MODERATE: This resolver has some privacy features but is missing key protections");
637
+ }
638
+ else if (score >= 25) {
639
+ findings.push("POOR: This resolver has limited privacy features");
640
+ }
641
+ else {
642
+ findings.push("CRITICAL: This resolver has virtually no privacy protections");
643
+ }
644
+ for (const check of checks) {
645
+ findings.push(`${check.passed ? "PASS" : "FAIL"}: ${check.name} (${check.weight} pts) — ${check.detail}`);
646
+ }
647
+ return json({
648
+ resolver,
649
+ privacy_score: score,
650
+ max_score: maxScore,
651
+ grade: score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F",
652
+ checks,
653
+ findings,
654
+ });
655
+ },
656
+ };
657
+ // ─── Tool 6: privacy_leak_test ───
658
+ const privacyLeakTest = {
659
+ name: "privacy_leak_test",
660
+ description: "DNS leak test: determines which resolver(s) your system is actually using by querying services that reveal resolver IP addresses. Compares observed resolver IPs against an expected resolver to detect leaks or misconfigurations.",
661
+ schema: {
662
+ expected_resolver: z
663
+ .string()
664
+ .optional()
665
+ .describe("Expected resolver IP address to compare against (e.g. '1.1.1.1'). If not provided, just reports which resolvers are seen."),
666
+ },
667
+ async execute(args) {
668
+ const expectedResolver = args.expected_resolver;
669
+ const resolverIps = [];
670
+ const probes = [];
671
+ // Probe 1: whoami.akamai.net TXT (returns the resolver IP that queried Akamai)
672
+ try {
673
+ const resolver = new dns.Resolver();
674
+ const results = await resolver.resolveTxt("whoami.akamai.net");
675
+ for (const r of results) {
676
+ const ip = r.join("").trim();
677
+ if (ip) {
678
+ resolverIps.push(ip);
679
+ probes.push({ source: "whoami.akamai.net TXT", result: ip, error: null });
680
+ }
681
+ }
682
+ }
683
+ catch (err) {
684
+ probes.push({ source: "whoami.akamai.net TXT", result: null, error: err.message });
685
+ }
686
+ // Probe 2: o-o.myaddr.l.google.com TXT (returns resolver IP info from Google)
687
+ try {
688
+ const resolver = new dns.Resolver();
689
+ const results = await resolver.resolveTxt("o-o.myaddr.l.google.com");
690
+ for (const r of results) {
691
+ const txt = r.join("").trim();
692
+ if (txt) {
693
+ // Google returns "edns0-client-subnet <ip>/prefix" or just the resolver IP
694
+ const ipMatch = txt.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/);
695
+ if (ipMatch) {
696
+ const ip = ipMatch[1];
697
+ if (!resolverIps.includes(ip))
698
+ resolverIps.push(ip);
699
+ }
700
+ probes.push({ source: "o-o.myaddr.l.google.com TXT", result: txt, error: null });
701
+ }
702
+ }
703
+ }
704
+ catch (err) {
705
+ probes.push({ source: "o-o.myaddr.l.google.com TXT", result: null, error: err.message });
706
+ }
707
+ // Probe 3: myip.opendns.com A (using OpenDNS resolver)
708
+ try {
709
+ const resolver = new dns.Resolver();
710
+ resolver.setServers(["208.67.222.222"]);
711
+ const results = await resolver.resolve4("myip.opendns.com");
712
+ for (const ip of results) {
713
+ probes.push({ source: "myip.opendns.com A (via OpenDNS)", result: ip, error: null });
714
+ // This returns YOUR public IP, not the resolver IP — still useful for leak detection
715
+ }
716
+ }
717
+ catch (err) {
718
+ probes.push({ source: "myip.opendns.com A (via OpenDNS)", result: null, error: err.message });
719
+ }
720
+ // Determine unique resolver IPs observed
721
+ const uniqueResolverIps = [...new Set(resolverIps)];
722
+ const findings = [];
723
+ if (uniqueResolverIps.length === 0) {
724
+ findings.push("WARNING: Could not determine resolver IPs from leak test probes");
725
+ }
726
+ else {
727
+ findings.push(`Detected resolver IP(s): ${uniqueResolverIps.join(", ")}`);
728
+ }
729
+ if (expectedResolver && uniqueResolverIps.length > 0) {
730
+ const matchesExpected = uniqueResolverIps.some((ip) => ip === expectedResolver);
731
+ const unexpectedIps = uniqueResolverIps.filter((ip) => ip !== expectedResolver);
732
+ if (matchesExpected && unexpectedIps.length === 0) {
733
+ findings.push(`OK: All observed resolver IPs match expected resolver (${expectedResolver})`);
734
+ }
735
+ else if (matchesExpected && unexpectedIps.length > 0) {
736
+ findings.push(`WARNING: DNS leak detected! Expected ${expectedResolver}, but also saw: ${unexpectedIps.join(", ")}`);
737
+ findings.push("Impact: DNS queries may be going to multiple resolvers, some unintended");
738
+ findings.push("Remediation: Check system DNS settings, VPN configuration, and browser DoH settings");
739
+ }
740
+ else {
741
+ findings.push(`WARNING: DNS leak detected! Expected ${expectedResolver}, but saw: ${uniqueResolverIps.join(", ")}`);
742
+ findings.push("Your DNS queries are NOT going through the expected resolver");
743
+ findings.push("Remediation: Verify DNS configuration in network settings and VPN client");
744
+ }
745
+ }
746
+ else if (!expectedResolver && uniqueResolverIps.length > 0) {
747
+ findings.push("INFO: No expected resolver specified — showing observed resolvers only");
748
+ if (uniqueResolverIps.length > 1) {
749
+ findings.push("NOTE: Multiple resolver IPs detected — queries may be load-balanced or leaked");
750
+ }
751
+ }
752
+ return json({
753
+ expected_resolver: expectedResolver ?? null,
754
+ observed_resolver_ips: uniqueResolverIps,
755
+ leak_detected: expectedResolver
756
+ ? !uniqueResolverIps.every((ip) => ip === expectedResolver) && uniqueResolverIps.length > 0
757
+ : null,
758
+ probes,
759
+ findings,
760
+ });
761
+ },
762
+ };
763
+ // ─── Export ───
764
+ export const privacyTools = [
765
+ privacyDohTest,
766
+ privacyDotTest,
767
+ privacyDoqTest,
768
+ privacyEcsLeak,
769
+ privacyResolverAudit,
770
+ privacyLeakTest,
771
+ ];
772
+ //# sourceMappingURL=index.js.map