Add local zildns service and ARM smoke validation
This commit is contained in:
+1
-1
Submodule PopuraDNS updated: 175379dd28...13731b16f8
@@ -101,7 +101,7 @@ The stack now provides DNS resolution for a comprehensive set of alternative TLD
|
||||
| `.bit` | namecoindns (10.5.0.12) via CoreDNS | Namecoin names (local resolver service) |
|
||||
| `.alt` | local unbound (10.5.0.5) via CoreDNS | Alternative DNS root (local-only fallback mode) |
|
||||
| `.loki` | local lokinet (10.5.0.14) via CoreDNS | Lokinet privacy network |
|
||||
| `.zil` | local unbound (10.5.0.5) via CoreDNS | Zilliqa blockchain (local-only fallback mode) |
|
||||
| `.zil` | zildns (10.5.0.15) via CoreDNS | Zilliqa blockchain (local resolver service) |
|
||||
| `.web3` | local unbound (10.5.0.5) via CoreDNS | General blockchain namespace (local-only fallback mode) |
|
||||
| Alfis TLDs | Alfis (10.5.0.3) | `.anon`, `.btn`, `.conf`, `.index`, `.merch`, `.mirror`, `.mob`, `.screen`, `.srv`, `.ygg` |
|
||||
| EmerCoin TLDs | EmerCoin (10.5.0.9) | `.emc`, `.coin`, `.lib`, `.bazar`, `.enum` |
|
||||
@@ -116,7 +116,9 @@ Darkproxy now avoids direct public DNS forwarders for `.alt`, `.zil`, `.web3`, O
|
||||
Important limitations still apply for authoritative data:
|
||||
|
||||
* `.eth` requires an Ethereum JSON-RPC backend. By default this stack uses `https://ethereum-rpc.publicnode.com` in `ensdns`.
|
||||
* `.alt`, OpenNIC TLDs, `.zil`, and `.web3` are not in the ICANN root. In local-only fallback mode they are sent to local Unbound, so they usually return `NXDOMAIN` unless you provide your own authoritative/local resolver backend for those namespaces.
|
||||
* `.alt`, OpenNIC TLDs, and `.web3` are not in the ICANN root. In local-only fallback mode they are sent to local Unbound, so they usually return `NXDOMAIN` unless you provide your own authoritative/local resolver backend for those namespaces.
|
||||
* `.zil` is resolved through local `zildns`, which queries ZNS using `https://api.zilliqa.com` by default. You can override this endpoint with `ZILDNS_ZNS_URL` in `docker-compose.yml`.
|
||||
* `zildns` supports `ZILDNS_CACHE_SECONDS` (record cache duration) and `ZILDNS_PREWARM_DOMAIN` (startup warm-up query) to reduce first-query latency.
|
||||
* `.loki` is resolved by local Lokinet, and Lokinet fallback DNS now points to local Unbound instead of a public resolver.
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ services:
|
||||
coredns:
|
||||
platform: linux/arm64
|
||||
|
||||
zildns:
|
||||
platform: linux/arm64
|
||||
|
||||
unbound:
|
||||
image: darkproxy-unbound:arm64
|
||||
build:
|
||||
|
||||
@@ -266,6 +266,27 @@ services:
|
||||
darkproxy:
|
||||
ipv4_address: 10.5.0.11
|
||||
|
||||
zildns:
|
||||
build:
|
||||
context: ./zildns
|
||||
dockerfile: Dockerfile
|
||||
container_name: darkzil
|
||||
restart: unless-stopped
|
||||
cpus: "0.5"
|
||||
mem_reservation: 64m
|
||||
mem_limit: 256m
|
||||
environment:
|
||||
ZILDNS_ZNS_URL: https://api.zilliqa.com
|
||||
ZILDNS_ZNS_NETWORK: mainnet
|
||||
ZILDNS_TTL: 120
|
||||
ZILDNS_CACHE_SECONDS: 300
|
||||
ZILDNS_PREWARM_DOMAIN: brad.zil
|
||||
sysctls:
|
||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||
networks:
|
||||
darkproxy:
|
||||
ipv4_address: 10.5.0.15
|
||||
|
||||
lokinet:
|
||||
build:
|
||||
context: ./lokinet
|
||||
|
||||
+14
-1
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE=(docker compose -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.arm.yml")
|
||||
|
||||
services=(dark3proxy darkdns darkscale darkpihole darki2p)
|
||||
services=(dark3proxy darkdns darkzil darkscale darkpihole darki2p)
|
||||
dns_queries=(google.com torproject.org github.com wikipedia.org example.onion example.i2p)
|
||||
|
||||
echo "[smoke] stack status"
|
||||
@@ -37,6 +37,19 @@ for pass in 1 2; do
|
||||
echo
|
||||
done
|
||||
|
||||
echo
|
||||
echo "[smoke] .zil path checks via coredns"
|
||||
for q in brad.zil not-a-real-name-xyz12345.zil; do
|
||||
out="$(docker exec darkpihole sh -lc "dig @10.5.0.4 +time=8 +tries=1 '$q' TXT 2>&1" || true)"
|
||||
status="$(printf "%s\n" "$out" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -n1)"
|
||||
qtime="$(printf "%s\n" "$out" | sed -n 's/;; Query time: \([0-9]*\).*/\1/p' | head -n1)"
|
||||
answers="$(printf "%s\n" "$out" | awk '/^;; ANSWER SECTION:/{flag=1;next}/^;;/{if(flag)exit}flag' | wc -l | tr -d ' ')"
|
||||
[[ -n "$status" ]] || status="NO_RESPONSE"
|
||||
[[ -n "$qtime" ]] || qtime="-1"
|
||||
[[ -n "$answers" ]] || answers="0"
|
||||
printf '%s status=%s query_ms=%s answers=%s\n' "$q" "$status" "$qtime" "$answers"
|
||||
done
|
||||
|
||||
echo "[smoke] key error scan"
|
||||
for c in "${services[@]}"; do
|
||||
echo "=== $c ==="
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM node:16-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json /app/package.json
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY server.js /app/server.js
|
||||
|
||||
EXPOSE 53/udp
|
||||
EXPOSE 53/tcp
|
||||
|
||||
CMD ["node", "/app/server.js"]
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "zildns",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Blockchain-backed DNS resolver for .zil domains",
|
||||
"license": "MIT",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"@unstoppabledomains/resolution": "7.1.4",
|
||||
"dns2": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
"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);
|
||||
});
|
||||
Reference in New Issue
Block a user