Files
auto-ci 21718042b6
Configuration validation / lint (push) Has been cancelled
Configuration validation / smoke (push) Has been cancelled
Harden release defaults and validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 19:29:00 -04:00

249 lines
6.1 KiB
JavaScript

"use strict";
const dns2 = require("dns2");
const { default: Resolution } = require("@unstoppabledomains/resolution");
const LISTEN_HOST = process.env.ZILDNS_LISTEN_HOST || "0.0.0.0";
const LISTEN_PORT = Number(process.env.ZILDNS_LISTEN_PORT || "53");
const DEFAULT_TTL = Number(process.env.ZILDNS_TTL || "120");
const CACHE_SECONDS = Number(process.env.ZILDNS_CACHE_SECONDS || "30");
const ZNS_URL = process.env.ZILDNS_ZNS_URL || "https://api.zilliqa.com";
const ZNS_NETWORK = process.env.ZILDNS_ZNS_NETWORK || "mainnet";
const PREWARM_DOMAIN = normalizePrewarmDomain(process.env.ZILDNS_PREWARM_DOMAIN || "brad.zil");
const { Packet, UDPServer, TCPServer } = dns2;
const RCODE = {
NOERROR: 0,
SERVFAIL: 2,
NXDOMAIN: 3,
};
const resolution = new Resolution({
sourceConfig: {
zns: {
url: ZNS_URL,
network: ZNS_NETWORK,
},
},
});
const cache = new Map();
function normalizeName(name) {
return String(name || "").replace(/\.$/, "").toLowerCase();
}
function normalizePrewarmDomain(name) {
const normalized = normalizeName(name);
return normalized.endsWith(".zil") ? normalized : "";
}
function cacheGet(key) {
const entry = cache.get(key);
if (!entry) {
return null;
}
if (Date.now() > entry.expiresAt) {
cache.delete(key);
return null;
}
return entry.value;
}
function cacheSet(key, value) {
cache.set(key, {
value,
expiresAt: Date.now() + CACHE_SECONDS * 1000,
});
}
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
)
]);
}
async function resolveZilDomain(domain) {
const cached = cacheGet(domain);
if (cached !== null) {
return cached;
}
try {
const [zilResult, ipfsResult] = await withTimeout(
Promise.allSettled([
resolution.addr(domain, "ZIL"),
resolution.ipfsHash(domain),
]),
5000 // 5 second timeout
);
const result = {
zilAddress: zilResult.status === "fulfilled" && zilResult.value ? zilResult.value : null,
ipfsHash: ipfsResult.status === "fulfilled" && ipfsResult.value ? ipfsResult.value : null,
};
const hasData = Boolean(result.zilAddress || result.ipfsHash);
const resolved = hasData ? result : null;
cacheSet(domain, resolved);
return resolved;
} catch (err) {
console.error(`[zildns] API failed for ${domain}:`, err.message);
// Cache negative result for a short time to avoid repeated API failures
cacheSet(domain, null);
return null;
}
}
function makeResponse(request) {
const response = Packet.createResponseFromRequest(request);
response.header.aa = 1;
response.header.ra = 1;
response.header.z = 0;
response.header.tc = 0;
return response;
}
function addTxtAnswers(response, name, resolved) {
if (resolved.zilAddress) {
response.answers.push({
name,
type: Packet.TYPE.TXT,
class: Packet.CLASS.IN,
ttl: DEFAULT_TTL,
data: `zil_address=${resolved.zilAddress}`,
});
}
if (resolved.ipfsHash) {
response.answers.push({
name,
type: Packet.TYPE.TXT,
class: Packet.CLASS.IN,
ttl: DEFAULT_TTL,
data: `ipfs_hash=${resolved.ipfsHash}`,
});
}
}
function addCnameAnswer(response, name, resolved) {
if (!resolved.ipfsHash) {
return;
}
response.answers.push({
name,
type: Packet.TYPE.CNAME,
class: Packet.CLASS.IN,
ttl: DEFAULT_TTL,
domain: `${resolved.ipfsHash}.ipfs.dweb.link.`,
});
}
async function buildResponse(request) {
const response = makeResponse(request);
const question = request.questions && request.questions[0];
if (!question) {
return response;
}
const qname = normalizeName(question.name);
const qtype = question.type;
const isType = (name) => qtype === name || qtype === Packet.TYPE[name];
if (!qname.endsWith(".zil")) {
response.header.rcode = RCODE.NXDOMAIN;
return response;
}
const resolved = await resolveZilDomain(qname);
if (!resolved) {
response.header.rcode = RCODE.NXDOMAIN;
return response;
}
if (isType("TXT") || isType("ANY")) {
addTxtAnswers(response, question.name, resolved);
}
if (isType("CNAME") || isType("A") || isType("AAAA") || isType("ANY")) {
addCnameAnswer(response, question.name, resolved);
}
return response;
}
async function maybePrewarm() {
if (!PREWARM_DOMAIN) {
console.log("[zildns] prewarm disabled");
return;
}
const started = Date.now();
try {
await resolveZilDomain(PREWARM_DOMAIN);
console.log(`[zildns] prewarm complete for ${PREWARM_DOMAIN} in ${Date.now() - started}ms`);
} catch (err) {
console.warn(
`[zildns] prewarm failed for ${PREWARM_DOMAIN}:`,
err && err.message ? err.message : String(err),
);
}
}
async function start() {
await maybePrewarm();
const udpServer = new UDPServer((request, send) => {
buildResponse(request)
.then((response) => {
send(response);
})
.catch((err) => {
console.error("[zildns] request failed:", err.message);
const response = makeResponse(request);
response.header.rcode = RCODE.SERVFAIL;
send(response);
});
});
const tcpServer = new TCPServer((request, send) => {
buildResponse(request)
.then((response) => {
send(response);
})
.catch((err) => {
console.error("[zildns] request failed:", err.message);
const response = makeResponse(request);
response.header.rcode = RCODE.SERVFAIL;
send(response);
});
});
udpServer.on("error", (err) => {
console.error("[zildns] udp error:", err);
});
tcpServer.on("error", (err) => {
console.error("[zildns] tcp error:", err);
});
udpServer.listen(LISTEN_PORT, LISTEN_HOST);
tcpServer.listen(LISTEN_PORT, LISTEN_HOST);
console.log(`[zildns] listening on ${LISTEN_HOST}:${LISTEN_PORT} (udp/tcp)`);
console.log(`[zildns] zns provider ${ZNS_URL} (${ZNS_NETWORK})`);
return { udpServer, tcpServer };
}
start().catch((err) => {
console.error("[zildns] startup failed:", err);
process.exit(1);
});