249 lines
6.1 KiB
JavaScript
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);
|
|
});
|