feat(namecoin): switch .bit resolution to true on-chain via local namecoind RPC
Configuration validation / lint (push) Has been cancelled
Configuration validation / smoke (push) Has been cancelled

This commit is contained in:
auto-ci
2026-03-01 22:47:32 -05:00
parent 958947b334
commit d66f208f0e
3 changed files with 119 additions and 40 deletions
+9 -5
View File
@@ -158,12 +158,13 @@ python3 scripts/resolve_ens.py ens.eth --include-text --pretty
```
## Local Namecoin (.bit) resolver
## Local Namecoin (.bit) resolver (true on-chain)
`.bit` lookups are routed through a local `namecoindns` service in the stack:
`.bit` lookups are routed through a local blockchain-backed resolver path:
* CoreDNS forwards `bit.:53` to `10.5.0.12:53`.
* `namecoindns` performs DNS-over-HTTPS upstream resolution.
* CoreDNS forwards `bit.:53` to `10.5.0.12:53` (`namecoindns`).
* `namecoindns` queries local `namecoind` JSON-RPC (`10.5.0.13:8336`).
* `namecoind` syncs the Namecoin blockchain and serves on-chain records.
Quick test:
@@ -173,7 +174,10 @@ docker exec darkpihole dig @10.5.0.4 TXT id.bit
```
If your environment returns `NXDOMAIN` for `.bit`, configure Namecoin-capable
DoH upstreams in `docker-compose.yml` under `namecoindns` (`NAMECOIN_DOH_UPSTREAMS`).
records in Namecoin itself and wait for local `namecoind` sync completion.
> **Important:** initial blockchain sync can take significant time. During sync,
> `.bit` responses may be empty or incomplete.
## Monitoring the proxy
+28 -2
View File
@@ -239,15 +239,39 @@ services:
darkproxy:
ipv4_address: 10.5.0.11
namecoind:
image: ukd1/namecoind:latest
container_name: darknamecoind
restart: unless-stopped
command:
- "-server=1"
- "-rpcbind=0.0.0.0"
- "-rpcallowip=10.5.0.0/16"
- "-rpcuser=namecoinrpc"
- "-rpcpassword=CHANGE_ME_NAMECOIN_RPC_PASSWORD"
- "-printtoconsole"
- "-txindex=1"
volumes:
- namecoin_data:/namecoin
sysctls:
- "net.ipv6.conf.all.disable_ipv6=0"
networks:
darkproxy:
ipv4_address: 10.5.0.13
namecoindns:
build:
context: ./namecoindns
dockerfile: Dockerfile
container_name: darknamecoin
restart: unless-stopped
depends_on:
- namecoind
environment:
NAMECOIN_DOH_TIMEOUT: 6
NAMECOIN_DOH_UPSTREAMS: https://dns.google/dns-query,https://cloudflare-dns.com/dns-query
NAMECOIN_RPC_URL: http://darknamecoind:8336/
NAMECOIN_RPC_USER: namecoinrpc
NAMECOIN_RPC_PASSWORD: CHANGE_ME_NAMECOIN_RPC_PASSWORD
NAMECOIN_RPC_TIMEOUT: 8
sysctls:
- "net.ipv6.conf.all.disable_ipv6=0"
networks:
@@ -320,6 +344,8 @@ volumes:
name: darkalfis_data
emc_data:
name: emc_data
namecoin_data:
name: namecoin_data
tor-data:
driver: local
tailscale_data:
+82 -33
View File
@@ -1,26 +1,20 @@
#!/usr/bin/env python3
import base64
import json
import os
import socket
import threading
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from dnslib import DNSHeader, DNSRecord, QTYPE, RCODE
from dnslib import A, AAAA, CNAME, DNSHeader, DNSRecord, QTYPE, RCODE, RR, TXT
LISTEN_HOST = os.getenv("NAMECOIN_DNS_LISTEN_HOST", "0.0.0.0")
LISTEN_PORT = int(os.getenv("NAMECOIN_DNS_LISTEN_PORT", "53"))
DOH_TIMEOUT = float(os.getenv("NAMECOIN_DOH_TIMEOUT", "6"))
RPC_TIMEOUT = float(os.getenv("NAMECOIN_RPC_TIMEOUT", "8"))
DOH_UPSTREAMS = [
u.strip()
for u in os.getenv(
"NAMECOIN_DOH_UPSTREAMS",
"https://dns.google/dns-query,https://cloudflare-dns.com/dns-query",
).split(",")
if u.strip()
]
RPC_URL = os.getenv("NAMECOIN_RPC_URL", "http://darknamecoind:8336/")
RPC_USER = os.getenv("NAMECOIN_RPC_USER", "namecoinrpc")
RPC_PASSWORD = os.getenv("NAMECOIN_RPC_PASSWORD", "CHANGE_ME_NAMECOIN_RPC_PASSWORD")
def is_bit_query(record: DNSRecord) -> bool:
@@ -28,23 +22,44 @@ def is_bit_query(record: DNSRecord) -> bool:
return qname.endswith(".bit")
def doh_query(payload: bytes) -> bytes | None:
encoded = base64.urlsafe_b64encode(payload).rstrip(b"=").decode("ascii")
query = urlencode({"dns": encoded})
def rpc_call(method: str, params: list):
payload = json.dumps({"jsonrpc": "1.0", "id": "namecoindns", "method": method, "params": params}).encode("utf-8")
req = Request(RPC_URL, data=payload, headers={"Content-Type": "application/json"})
token = (f"{RPC_USER}:{RPC_PASSWORD}").encode("utf-8")
import base64
for upstream in DOH_UPSTREAMS:
url = f"{upstream}?{query}"
req = Request(url, headers={"accept": "application/dns-message", "user-agent": "darkproxy-namecoindns/1.0"})
try:
with urlopen(req, timeout=DOH_TIMEOUT) as resp:
if resp.status != 200:
continue
body = resp.read()
if body:
return body
except Exception:
continue
return None
req.add_header("Authorization", "Basic " + base64.b64encode(token).decode("ascii"))
with urlopen(req, timeout=RPC_TIMEOUT) as resp:
body = resp.read()
result = json.loads(body.decode("utf-8"))
if result.get("error"):
raise RuntimeError(str(result["error"]))
return result.get("result")
def lookup_namecoin_bit(label: str):
key = f"d/{label}"
try:
name_info = rpc_call("name_show", [key])
except Exception:
return None
value = name_info.get("value")
if not value:
return None
try:
parsed = json.loads(value)
except Exception:
return None
if isinstance(parsed, dict) and "map" in parsed and isinstance(parsed["map"], dict):
parsed = parsed["map"].get("", parsed)
if not isinstance(parsed, dict):
return None
return parsed
def fail_response(payload: bytes) -> bytes:
@@ -63,10 +78,44 @@ def handle_packet(payload: bytes) -> bytes:
reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, ra=1, aa=0, rcode=RCODE.NXDOMAIN), q=req.q)
return reply.pack()
upstream = doh_query(payload)
if upstream:
return upstream
return fail_response(payload)
qname = str(req.q.qname).rstrip(".").lower()
label = qname[: -len(".bit")].strip(".")
if not label:
return fail_response(payload)
record = lookup_namecoin_bit(label)
if record is None:
reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, ra=1, aa=1, rcode=RCODE.NXDOMAIN), q=req.q)
return reply.pack()
reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, ra=1, aa=1, rcode=RCODE.NOERROR), q=req.q)
fqdn = str(req.q.qname)
qtype = QTYPE[req.q.qtype]
ttl = 60
ip = record.get("ip")
ips = ip if isinstance(ip, list) else ([ip] if isinstance(ip, str) else [])
for item in ips:
if ":" in item:
if qtype in ("AAAA", "ANY"):
reply.add_answer(RR(fqdn, QTYPE.AAAA, rdata=AAAA(item), ttl=ttl))
else:
if qtype in ("A", "ANY"):
reply.add_answer(RR(fqdn, QTYPE.A, rdata=A(item), ttl=ttl))
alias = record.get("alias") or record.get("translate")
if isinstance(alias, str) and alias:
target = alias if alias.endswith(".") else alias + "."
if qtype in ("CNAME", "A", "AAAA", "ANY"):
reply.add_answer(RR(fqdn, QTYPE.CNAME, rdata=CNAME(target), ttl=ttl))
if qtype in ("TXT", "ANY"):
reply.add_answer(RR(fqdn, QTYPE.TXT, rdata=TXT(json.dumps(record, separators=(",", ":"))), ttl=ttl))
if len(reply.rr) == 0:
return fail_response(payload)
return reply.pack()
def serve_udp(sock: socket.socket):
@@ -100,7 +149,7 @@ def serve_tcp(sock: socket.socket):
def main():
print(f"[namecoindns] listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True)
print(f"[namecoindns] DoH upstreams: {', '.join(DOH_UPSTREAMS)}", flush=True)
print(f"[namecoindns] RPC endpoint: {RPC_URL}", flush=True)
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.bind((LISTEN_HOST, LISTEN_PORT))