226 lines
5.2 KiB
JavaScript
226 lines
5.2 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, createServer } = 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,
|
|
});
|
|
}
|
|
|
|
async function resolveZilDomain(domain) {
|
|
const cached = cacheGet(domain);
|
|
if (cached !== null) {
|
|
return cached;
|
|
}
|
|
|
|
const [zilResult, ipfsResult] = await Promise.allSettled([
|
|
resolution.addr(domain, "ZIL"),
|
|
resolution.ipfsHash(domain),
|
|
]);
|
|
|
|
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;
|
|
}
|
|
|
|
function makeResponse(request) {
|
|
const response = Packet.createResponseFromRequest(request);
|
|
response.header.aa = 1;
|
|
response.header.ra = 1;
|
|
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 handleRequest(request, send) {
|
|
const response = makeResponse(request);
|
|
const question = request.questions && request.questions[0];
|
|
|
|
if (!question) {
|
|
send(response);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
send(response);
|
|
return;
|
|
}
|
|
|
|
const resolved = await resolveZilDomain(qname);
|
|
if (!resolved) {
|
|
response.header.rcode = RCODE.NXDOMAIN;
|
|
send(response);
|
|
return;
|
|
}
|
|
|
|
if (isType("TXT") || isType("ANY")) {
|
|
addTxtAnswers(response, question.name, resolved);
|
|
}
|
|
|
|
if (isType("CNAME") || isType("A") || isType("AAAA") || isType("ANY")) {
|
|
addCnameAnswer(response, question.name, resolved);
|
|
}
|
|
|
|
send(response);
|
|
}
|
|
|
|
const server = createServer({
|
|
udp: true,
|
|
tcp: true,
|
|
handle: (request, send) => {
|
|
handleRequest(request, send).catch((err) => {
|
|
const response = makeResponse(request);
|
|
response.header.rcode = RCODE.SERVFAIL;
|
|
send(response);
|
|
console.error("[zildns] request failed:", err && err.message ? err.message : String(err));
|
|
});
|
|
},
|
|
});
|
|
|
|
server.on("listening", () => {
|
|
console.log(`[zildns] listening on ${LISTEN_HOST}:${LISTEN_PORT} (udp/tcp)`);
|
|
console.log(`[zildns] zns provider ${ZNS_URL} (${ZNS_NETWORK})`);
|
|
});
|
|
|
|
server.on("close", () => {
|
|
console.log("[zildns] server closed");
|
|
});
|
|
|
|
server.on("error", (err) => {
|
|
console.error("[zildns] server error:", err);
|
|
});
|
|
|
|
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();
|
|
|
|
server.listen({
|
|
udp: {
|
|
address: LISTEN_HOST,
|
|
port: LISTEN_PORT,
|
|
},
|
|
tcp: {
|
|
address: LISTEN_HOST,
|
|
port: LISTEN_PORT,
|
|
},
|
|
});
|
|
}
|
|
|
|
start().catch((err) => {
|
|
console.error("[zildns] startup failed:", err);
|
|
process.exit(1);
|
|
});
|