Skip to content

Instantly share code, notes, and snippets.

@shazron
Created August 21, 2025 06:34
Show Gist options
  • Save shazron/5e5d456a9dc0efa25f50233c867b6db8 to your computer and use it in GitHub Desktop.
Save shazron/5e5d456a9dc0efa25f50233c867b6db8 to your computer and use it in GitHub Desktop.
DNS Resolver Diagnostics
#!/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