Skip to content

Instantly share code, notes, and snippets.

@jamestharpe
Created March 8, 2025 19:29
Show Gist options
  • Save jamestharpe/2568f47831519a2e74674a4b12761eec to your computer and use it in GitHub Desktop.
Save jamestharpe/2568f47831519a2e74674a4b12761eec to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
/**
* Usage: node get-dns.js <domain>
* Example: node get-dns.js crittercontrol.com
*
* This script:
* 1. Queries DNS records for the apex domain and a list of possible subdomains.
* 2. Ensures the apex domain (@) always has an SOA record, creating a default if none is found.
* 3. Filters out invalid record conflicts (CNAME vs. A/AAAA/MX/etc. on the same label).
* 4. Generates a zone file with $TTL and $ORIGIN at the top, referencing subdomains as relative labels.
* 5. Outputs a summary of how many DNS records and how many domain labels were discovered.
*/
const dns = require("dns").promises;
const fs = require("fs");
const path = require("path");
// Default TTL (in seconds)
const DEFAULT_TTL = 3600;
// List of record types to query
const RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SOA", "SRV"];
// Expand our subdomain dictionary:
const SUBDOMAINS = [
"_domainconnect",
"about",
"account",
"accounts",
"admin",
"ads",
"advertising",
"alpha",
"analysis",
"analytics",
"api",
"app",
"apps",
"assets",
"au",
"auth",
"autoconfig",
"autodiscover",
"backend",
"beta",
"billing",
"blog",
"board",
"ca",
"careers",
"cart",
"cdn",
"chat",
"checkout",
"ci",
"cicd",
"cloud",
"community",
"console",
"cpanel",
"dashboard",
"de",
"demo",
"dev",
"developer",
"developers",
"development",
"devops",
"discussion",
"docs",
"download",
"downloads",
"en",
"es",
"events",
"extranet",
"faq",
"feedback",
"file",
"files",
"forum",
"forums",
"fr",
"frontend",
"ftp",
"get",
"git",
"github",
"gitlab",
"go",
"gql",
"graphql",
"help",
"helpdesk",
"hr",
"images",
"imap",
"img",
"internal",
"intranet",
"investor",
"investors",
"invoice",
"invoices",
"jobs",
"join",
"kb",
"labs",
"landing",
"lists",
"live",
"login",
"m",
"mail",
"market",
"media",
"metrics",
"mobile",
"monitor",
"my",
"news",
"oauth",
"opportunities",
"panel",
"partners",
"payment",
"payments",
"pop",
"portal",
"pre-production",
"preprod",
"preprod-env",
"press",
"private",
"promo",
"qa",
"register",
"rss",
"sandbox",
"secure",
"security",
"server",
"sftp",
"shop",
"signin",
"signup",
"slack",
"smtp",
"sso",
"stage",
"staging",
"static",
"statistics",
"stats",
"status",
"store",
"supplier",
"support",
"test",
"testing",
"tickets",
"track",
"tracking",
"try",
"uk",
"upload",
"uploads",
"us",
"vendors",
"vpn",
"webdisk",
"webmail",
"what",
"whm",
"who",
"why",
"www",
];
// Retrieve DNS records for a single domain across multiple record types
async function getAllRecordsForDomain(domain) {
const results = {};
for (const type of RECORD_TYPES) {
try {
const records = await dns.resolve(domain, type);
if (records && records.length > 0) {
results[type] = records;
}
} catch (err) {
// Domain doesn't have that record type or there's an error. Ignore.
}
}
return results;
}
/**
* Convert a fully qualified hostname into a "relative" label for the apex domain.
* E.g.:
* domain = "example.com"
* getRelativeName("example.com", "example.com") -> "@"
* getRelativeName("mail.example.com", "example.com") -> "mail"
*/
function getRelativeName(hostname, apexDomain) {
if (hostname === apexDomain) return "@";
const suffix = "." + apexDomain;
if (hostname.endsWith(suffix)) {
return hostname.slice(0, -suffix.length);
}
// Fallback: return the entire hostname if it doesn't match exactly
return hostname;
}
/**
* Generate a default SOA record object if none is found.
* Adjust values (nsname, hostmaster, etc.) as desired.
*/
function generateDefaultSoa(domain) {
const now = new Date();
// Format an ISO-like date string: YYYYMMDDnn (nn = 2-digit counter)
const datePart = now.toISOString().slice(0, 10).replace(/-/g, "");
// Minimal approach: take today's date and add 01
const serial = parseInt(`${datePart}01`, 10);
return {
nsname: `ns1.${domain}`, // Primary nameserver
hostmaster: `hostmaster.${domain}`, // Hostmaster email -> hostmaster@domain
serial,
refresh: 7200, // 2 hours
retry: 3600, // 1 hour
expire: 1209600, // 14 days
minttl: 3600, // 1 hour
};
}
(async function main() {
if (process.argv.length < 3) {
console.error("Usage: node get-dns.js <domain>");
process.exit(1);
}
const domain = process.argv[2].toLowerCase().trim();
// 1. Gather DNS records for the apex domain
const recordsMap = {};
recordsMap[domain] = await getAllRecordsForDomain(domain);
// 2. Gather DNS records for subdomains
const subdomainList = SUBDOMAINS.map((sub) => `${sub}.${domain}`);
for (const sub of subdomainList) {
const subRecords = await getAllRecordsForDomain(sub);
if (Object.keys(subRecords).length > 0) {
recordsMap[sub] = subRecords;
}
}
// 3. If apex domain does not have an SOA record, add a default
if (!recordsMap[domain].SOA) {
recordsMap[domain].SOA = [generateDefaultSoa(domain)];
}
// 4. Remove invalid record conflicts
// If a label has a CNAME, we discard other record types for that label
for (const [hostname, recordTypes] of Object.entries(recordsMap)) {
if (recordTypes.CNAME && recordTypes.CNAME.length > 0) {
// Keep the CNAME; discard the rest
const cnameRecords = recordTypes.CNAME;
Object.keys(recordTypes).forEach((type) => delete recordTypes[type]);
recordTypes.CNAME = cnameRecords;
}
}
// 5. Build a single zone file with a single $ORIGIN and $TTL
const zoneLines = [];
zoneLines.push(`$TTL ${DEFAULT_TTL}`);
zoneLines.push(`$ORIGIN ${domain}.`);
// We'll collect lines for each label
const recordLines = [];
for (const [hostname, recordSet] of Object.entries(recordsMap)) {
const relativeName = getRelativeName(hostname, domain);
for (const [type, values] of Object.entries(recordSet)) {
for (const val of values) {
switch (type) {
case "A":
case "AAAA":
recordLines.push(`${relativeName} IN ${type} ${val}`);
break;
case "CNAME":
// Must ensure it ends with a dot if referencing a FQDN
recordLines.push(
`${relativeName} IN CNAME ${val.endsWith(".") ? val : val + "."}`
);
break;
case "MX":
// E.g. "mail IN MX priority mailhost."
if (typeof val === "object" && val.exchange) {
recordLines.push(
`${relativeName} IN MX ${val.priority} ${
val.exchange.endsWith(".") ? val.exchange : val.exchange + "."
}`
);
} else {
recordLines.push(`${relativeName} IN MX ${val}`);
}
break;
case "NS":
recordLines.push(
`${relativeName} IN NS ${val.endsWith(".") ? val : val + "."}`
);
break;
case "SOA":
// Node returns an object: { nsname, hostmaster, serial, refresh, retry, expire, minttl }
if (typeof val === "object") {
const {
nsname,
hostmaster,
serial,
refresh,
retry,
expire,
minttl,
} = val;
recordLines.push(
`${relativeName} IN SOA ${nsname}. ${hostmaster}. (` +
`${serial} ${refresh} ${retry} ${expire} ${minttl})`
);
} else {
recordLines.push(`${relativeName} IN SOA ${val}`);
}
break;
case "SRV":
// E.g. "service IN SRV priority weight port target."
if (typeof val === "object") {
const { priority, weight, port, name } = val;
recordLines.push(
`${relativeName} IN SRV ${priority} ${weight} ${port} ${
name.endsWith(".") ? name : name + "."
}`
);
} else {
recordLines.push(`${relativeName} IN SRV ${val}`);
}
break;
case "TXT":
// Node returns arrays of arrays for TXT
if (Array.isArray(val)) {
const textValue = val.join("");
recordLines.push(`${relativeName} IN TXT "${textValue}"`);
} else {
recordLines.push(`${relativeName} IN TXT "${val}"`);
}
break;
default:
recordLines.push(`${relativeName} IN ${type} ${val}`);
}
}
}
}
// 6. Sort the records so that '@' is at the top, optionally
recordLines.sort((a, b) => {
// Put lines starting with '@' first
if (a.startsWith("@") && !b.startsWith("@")) return -1;
if (!a.startsWith("@") && b.startsWith("@")) return 1;
return a.localeCompare(b);
});
zoneLines.push(...recordLines);
// 7. Write the zone data to a file
const zoneFileName = `${domain}.zone`;
fs.writeFileSync(
path.join(process.cwd(), zoneFileName),
zoneLines.join("\n") + "\n"
);
// 8. Summarize
let totalRecords = 0;
for (const recordObj of Object.values(recordsMap)) {
totalRecords += Object.values(recordObj).reduce(
(sum, arr) => sum + arr.length,
0
);
}
const totalHostnames = Object.keys(recordsMap).length;
console.log(
`Found ${totalRecords} DNS entries across ${totalHostnames} domain(s) for ${domain} and generated ${zoneFileName}`
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment