Created
August 21, 2025 06:34
-
-
Save shazron/5e5d456a9dc0efa25f50233c867b6db8 to your computer and use it in GitHub Desktop.
DNS Resolver Diagnostics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * ================================================================ | |
| * DNS + HTTPS Diagnostic Script for Node.js (Linear Version) | |
| * ================================================================ | |
| * | |
| * Description: | |
| * This script performs DNS and network diagnostics for a given host. | |
| * It tests: | |
| * 1. System DNS resolution (IPv4, IPv6, any) | |
| * 2. Google/Cloudflare DNS resolution | |
| * 3. HTTPS connectivity (IPv4, IPv6, or generic) including: | |
| * - HTTP method support (GET, HEAD, etc.) | |
| * - Custom path support | |
| * - Timeout, retries, and max redirect handling | |
| * - Full redirect chain logging | |
| * | |
| * CLI Arguments and Flags: | |
| * <hostname> Required. The host to diagnose (e.g., example.com) | |
| * | |
| * --path <path> Optional. Path to test for HTTPS requests (default: "/") | |
| * --method <HTTP_METHOD> Optional. HTTP method to use for HTTPS requests (default: "GET") | |
| * --timeout <ms> Optional. Timeout for each test in milliseconds (default: 5000) | |
| * --retries <n> Optional. Number of retries for failed DNS or HTTPS tests (default: 2) | |
| * --max-redirects <n> Optional. Maximum number of redirects to follow in HTTPS requests (default: 5) | |
| * --out <file.json> Optional. File path to save the JSON report | |
| * --json-only Optional. Output only JSON (suppresses console logging) | |
| * --help Show this usage text | |
| * | |
| * Example Usage: | |
| * node dns-diagnostic.js example.com | |
| * node dns-diagnostic.js example.com --path /status --method HEAD --timeout 3000 --retries 3 --max-redirects 5 | |
| * node dns-diagnostic.js example.com --json-only --out report.json | |
| * node dns-diagnostic.js --help | |
| * | |
| * Notes: | |
| * - Uses Node.js built-in modules only | |
| * - Supports IPv4 and IPv6 testing separately | |
| * - Outputs a detailed JSON report including DNS resolution and HTTPS results | |
| * - Exit codes: | |
| * 0 -> All DNS and HTTPS tests passed | |
| * 1 -> System DNS failed, Google DNS worked | |
| * 2 -> Both system and Google DNS failed | |
| * 3 -> System DNS worked, Google DNS failed (possible split-horizon DNS) | |
| * | |
| * ================================================================ | |
| */ | |
| const dns = require("node:dns").promises; | |
| const https = require("node:https"); | |
| const fs = require("node:fs"); | |
| // ----------------- Print Usage from Comment Block ----------------- | |
| function printUsageAndExit() { | |
| const scriptContent = fs.readFileSync(__filename, "utf-8"); | |
| const usageMatch = scriptContent.match(/\/\*\*([\s\S]*?)\*\//); | |
| if (usageMatch) { | |
| let usageText = usageMatch[1].trim(); | |
| usageText = usageText.replace(/^(\s*\* ?)(.*?):/gm, (m, p1, p2) => `${p1}\x1b[1m${p2}:\x1b[0m`); | |
| usageText = usageText.replace(/(--[a-zA-Z0-9\-]+)/g, "\x1b[36m$1\x1b[0m"); | |
| usageText = usageText.replace(/(Example Usage:)/g, "\x1b[33m$1\x1b[0m"); | |
| console.log(usageText); | |
| } else { | |
| console.log("\x1b[33mUsage: node dns-diagnostic.js <hostname> [flags]\x1b[0m"); | |
| } | |
| process.exit(0); | |
| } | |
| if (process.argv.includes("--help")) { | |
| printUsageAndExit(); | |
| } | |
| // ----------------- Flag parser ----------------- | |
| function parseFlags(argv) { | |
| const flags = { | |
| path: "/", | |
| method: "GET", | |
| timeout: 5000, | |
| retries: 2, | |
| maxRedirects: 5, | |
| out: null, | |
| jsonOnly: false, | |
| host: null | |
| }; | |
| if (argv.length === 0) { | |
| throw new Error("Usage: node dns-diagnostic.js <hostname> [--path <path>] [--method <HTTP>] [--timeout ms] [--retries n] [--max-redirects n] [--out file.json] [--json-only]"); | |
| } | |
| flags.host = argv[0]; | |
| for (let i = 1; i < argv.length; i++) { | |
| const arg = argv[i]; | |
| switch (arg) { | |
| case "--path": if (argv[i + 1]) flags.path = argv[++i]; break; | |
| case "--method": if (argv[i + 1]) flags.method = argv[++i].toUpperCase(); break; | |
| case "--timeout": if (argv[i + 1]) flags.timeout = parseInt(argv[++i], 10); break; | |
| case "--retries": if (argv[i + 1]) flags.retries = parseInt(argv[++i], 10); break; | |
| case "--max-redirects": if (argv[i + 1]) flags.maxRedirects = parseInt(argv[++i], 10); break; | |
| case "--out": if (argv[i + 1]) flags.out = argv[++i]; break; | |
| case "--json-only": flags.jsonOnly = true; break; | |
| default: break; | |
| } | |
| } | |
| return flags; | |
| } | |
| // ----------------- Utility ----------------- | |
| function log(...msg) { if (!flags.jsonOnly) console.log(...msg); } | |
| function logErr(...msg) { if (!flags.jsonOnly) console.error(...msg); } | |
| function withTimeout(promise, ms) { | |
| return new Promise((resolve, reject) => { | |
| const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms); | |
| promise.then(res => { clearTimeout(timer); resolve(res); }).catch(err => { clearTimeout(timer); reject(err); }); | |
| }); | |
| } | |
| async function retry(fn, label, maxRetries = 2) { | |
| let attempt = 0; | |
| while (attempt <= maxRetries) { | |
| try { return await fn(); } | |
| catch (err) { | |
| attempt++; | |
| if (attempt > maxRetries) throw err; | |
| logErr(`⚠️ Retry ${attempt}/${maxRetries} for ${label} failed: ${err.message || err.detail}`); | |
| } | |
| } | |
| } | |
| // ----------------- HTTPS Test ----------------- | |
| async function testHttpsIP(hostname, family = 4, redirectCount = 0, redirects = []) { | |
| const url = `https://${hostname}${flags.path}`; | |
| redirects.push(url); | |
| return withTimeout( | |
| new Promise((resolve, reject) => { | |
| const options = { host: hostname, family, port: 443, path: flags.path, method: flags.method }; | |
| https.get(options, (res) => { | |
| if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | |
| if (redirectCount >= flags.maxRedirects) { | |
| return reject({ status: "FAIL", url, method: flags.method, redirects, detail: `Too many redirects (> ${flags.maxRedirects})` }); | |
| } | |
| const newUrl = new URL(res.headers.location, url); | |
| return resolve(testHttpsIP(newUrl.hostname, family, redirectCount + 1, redirects)); | |
| } else { | |
| resolve({ status: `status ${res.statusCode}`, url, method: flags.method, redirects }); | |
| } | |
| }).on("error", (err) => { | |
| reject({ status: "FAIL", url, method: flags.method, redirects, detail: err.message }); | |
| }); | |
| }), | |
| flags.timeout | |
| ); | |
| } | |
| // ----------------- DNS + HTTPS Tests ----------------- | |
| async function runTests(label, servers = []) { | |
| log(`\n=== ${label} ===`); | |
| if (servers.length) dns.setServers(servers); | |
| const results = {}; | |
| async function logDnsResult(testName, promise) { | |
| try { | |
| const result = await retry(() => withTimeout(promise, flags.timeout), testName, flags.retries); | |
| results[testName] = { status: "OK", detail: result }; | |
| log(`\x1b[32m${testName}: ${JSON.stringify(result)}\x1b[0m`); | |
| } catch (err) { | |
| results[testName] = { status: "FAIL", detail: err.message }; | |
| logErr(`\x1b[31m${testName} failed: ${err.message}\x1b[0m`); | |
| } | |
| } | |
| await logDnsResult("dns.lookup", dns.lookup(flags.host).then(r => `${r.address} (IPv${r.family})`)); | |
| await logDnsResult("dns.resolve4", dns.resolve4(flags.host)); | |
| await logDnsResult("dns.resolve6", dns.resolve6(flags.host)); | |
| await logDnsResult("dns.resolveAny", dns.resolveAny(flags.host)); | |
| async function runHttpsTest(family, label) { | |
| try { | |
| const r = await retry(() => testHttpsIP(flags.host, family), `HTTPS ${label}`, flags.retries); | |
| const success = r.status.startsWith("status"); | |
| const color = success ? "\x1b[32m" : "\x1b[31m"; | |
| results[`https${family || ""}`] = { status: success ? "OK" : "FAIL", detail: r.status, url: r.url, method: r.method, redirects: r.redirects }; | |
| log(`${color}${label}: ${r.status} (${r.method} ${r.url})\x1b[0m`); | |
| if (r.redirects.length > 1) log(` ↪ Redirect chain: \x1b[36m${r.redirects.join(" -> ")}\x1b[0m`); | |
| } catch (err) { | |
| results[`https${family || ""}`] = { status: "FAIL", detail: err.detail, url: err.url, method: err.method, redirects: err.redirects }; | |
| logErr(`\x1b[31m${label} failed: ${err.detail} (${err.method} ${err.url})\x1b[0m`); | |
| if (err.redirects.length > 1) logErr(` ↪ Redirect chain: \x1b[36m${err.redirects.join(" -> ")}\x1b[0m`); | |
| } | |
| } | |
| if (results.resolve4?.status === "OK") await runHttpsTest(4, "HTTPS IPv4"); | |
| if (results.resolve6?.status === "OK") await runHttpsTest(6, "HTTPS IPv6"); | |
| if (!results.https4 && !results.https6) await runHttpsTest(undefined, "HTTPS generic"); | |
| return results; | |
| } | |
| // ----------------- Analysis ----------------- | |
| function analyze(systemResults, googleResults) { | |
| log("\n🔎 Summary Analysis:"); | |
| const sysFail = Object.values(systemResults).some((v) => v?.status === "FAIL"); | |
| const gooFail = Object.values(googleResults).some((v) => v?.status === "FAIL"); | |
| let summary = "", exitCode = 0; | |
| if (sysFail && !gooFail) { summary = "⚠️ System DNS failed but Google DNS worked → likely local config issue"; exitCode = 1; } | |
| else if (sysFail && gooFail) { summary = "❌ Both system and Google DNS failed → host may not exist or blocked"; exitCode = 2; } | |
| else if (!sysFail && gooFail) { summary = "🤔 System DNS worked but Google DNS failed → possible split-horizon DNS"; exitCode = 3; } | |
| else { summary = "✅ Both system and Google DNS worked → DNS is fine"; } | |
| log(summary); | |
| return { summary, exitCode }; | |
| } | |
| // ----------------- Summary Line with DNS vs HTTPS ----------------- | |
| function summarizeResults(systemResults, googleResults) { | |
| const allResults = [...Object.values(systemResults), ...Object.values(googleResults)]; | |
| let dnsPassed = 0, dnsFailed = 0; | |
| let httpsPassed = 0, httpsFailed = 0; | |
| allResults.forEach((r) => { | |
| if (r.status === "OK") { | |
| if (r.url) httpsPassed++; else dnsPassed++; | |
| } else { | |
| if (r.url) httpsFailed++; else dnsFailed++; | |
| } | |
| }); | |
| const totalPassed = dnsPassed + httpsPassed; | |
| const totalFailed = dnsFailed + httpsFailed; | |
| const color = totalFailed === 0 ? "\x1b[32m" : "\x1b[31m"; | |
| log(`${color}📊 Total Summary: DNS ${dnsPassed}/${dnsPassed + dnsFailed} passed, HTTPS ${httpsPassed}/${httpsPassed + httpsFailed} passed, overall ${totalPassed + totalFailed} tests, ${totalFailed} failed\x1b[0m`); | |
| } | |
| // ----------------- Main ----------------- | |
| const flags = parseFlags(process.argv.slice(2)); | |
| async function runDiagnostics() { | |
| log(`🔍 Diagnosing host: ${flags.host} (timeout: ${flags.timeout}ms, retries: ${flags.retries}, max redirects: ${flags.maxRedirects})`); | |
| const systemResults = await runTests("System Resolver"); | |
| const googleResults = await runTests("Google/Cloudflare DNS", ["8.8.8.8", "1.1.1.1"]); | |
| const { summary, exitCode } = analyze(systemResults, googleResults); | |
| summarizeResults(systemResults, googleResults); | |
| const report = { host: flags.host, path: flags.path, method: flags.method, timeout: flags.timeout, retries: flags.retries, maxRedirects: flags.maxRedirects, results: { system: systemResults, google: googleResults }, summary, exitCode }; | |
| if (!flags.jsonOnly) log("\n📊 JSON Report:"); | |
| console.log(JSON.stringify(report, null, 2)); | |
| if (flags.out) { | |
| try { fs.writeFileSync(flags.out, JSON.stringify(report, null, 2)); log(`\n💾 JSON report written to ${flags.out}`); } | |
| catch (err) { logErr(`\n❌ Failed to write JSON report: ${err.message}`); } | |
| } | |
| log("\n✅ Diagnostics complete."); | |
| process.exit(exitCode); | |
| } | |
| runDiagnostics(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment