Add local zildns service and ARM smoke validation
Configuration validation / lint (push) Has been cancelled
Configuration validation / smoke (push) Has been cancelled

This commit is contained in:
auto-ci
2026-03-06 22:39:30 -05:00
parent 8174346537
commit ec3c2d92cf
8 changed files with 293 additions and 4 deletions
Submodule PopuraDNS updated: 175379dd28...13731b16f8
+4 -2
View File
@@ -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.
+3
View File
@@ -39,6 +39,9 @@ services:
coredns:
platform: linux/arm64
zildns:
platform: linux/arm64
unbound:
image: darkproxy-unbound:arm64
build:
+21
View File
@@ -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
View File
@@ -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 ==="
+13
View File
@@ -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"]
+12
View File
@@ -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"
}
}
+225
View File
@@ -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);
});