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`. * CoreDNS forwards `bit.:53` to `10.5.0.12:53` (`namecoindns`).
* `namecoindns` performs DNS-over-HTTPS upstream resolution. * `namecoindns` queries local `namecoind` JSON-RPC (`10.5.0.13:8336`).
* `namecoind` syncs the Namecoin blockchain and serves on-chain records.
Quick test: 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 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 ## Monitoring the proxy
+28 -2
View File
@@ -239,15 +239,39 @@ services:
darkproxy: darkproxy:
ipv4_address: 10.5.0.11 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: namecoindns:
build: build:
context: ./namecoindns context: ./namecoindns
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: darknamecoin container_name: darknamecoin
restart: unless-stopped restart: unless-stopped
depends_on:
- namecoind
environment: environment:
NAMECOIN_DOH_TIMEOUT: 6 NAMECOIN_RPC_URL: http://darknamecoind:8336/
NAMECOIN_DOH_UPSTREAMS: https://dns.google/dns-query,https://cloudflare-dns.com/dns-query NAMECOIN_RPC_USER: namecoinrpc
NAMECOIN_RPC_PASSWORD: CHANGE_ME_NAMECOIN_RPC_PASSWORD
NAMECOIN_RPC_TIMEOUT: 8
sysctls: sysctls:
- "net.ipv6.conf.all.disable_ipv6=0" - "net.ipv6.conf.all.disable_ipv6=0"
networks: networks:
@@ -320,6 +344,8 @@ volumes:
name: darkalfis_data name: darkalfis_data
emc_data: emc_data:
name: emc_data name: emc_data
namecoin_data:
name: namecoin_data
tor-data: tor-data:
driver: local driver: local
tailscale_data: tailscale_data:
+82 -33
View File
@@ -1,26 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import base64 import json
import os import os
import socket import socket
import threading import threading
from urllib.parse import urlencode
from urllib.request import Request, urlopen 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_HOST = os.getenv("NAMECOIN_DNS_LISTEN_HOST", "0.0.0.0")
LISTEN_PORT = int(os.getenv("NAMECOIN_DNS_LISTEN_PORT", "53")) 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 = [ RPC_URL = os.getenv("NAMECOIN_RPC_URL", "http://darknamecoind:8336/")
u.strip() RPC_USER = os.getenv("NAMECOIN_RPC_USER", "namecoinrpc")
for u in os.getenv( RPC_PASSWORD = os.getenv("NAMECOIN_RPC_PASSWORD", "CHANGE_ME_NAMECOIN_RPC_PASSWORD")
"NAMECOIN_DOH_UPSTREAMS",
"https://dns.google/dns-query,https://cloudflare-dns.com/dns-query",
).split(",")
if u.strip()
]
def is_bit_query(record: DNSRecord) -> bool: def is_bit_query(record: DNSRecord) -> bool:
@@ -28,23 +22,44 @@ def is_bit_query(record: DNSRecord) -> bool:
return qname.endswith(".bit") return qname.endswith(".bit")
def doh_query(payload: bytes) -> bytes | None: def rpc_call(method: str, params: list):
encoded = base64.urlsafe_b64encode(payload).rstrip(b"=").decode("ascii") payload = json.dumps({"jsonrpc": "1.0", "id": "namecoindns", "method": method, "params": params}).encode("utf-8")
query = urlencode({"dns": encoded}) 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: req.add_header("Authorization", "Basic " + base64.b64encode(token).decode("ascii"))
url = f"{upstream}?{query}" with urlopen(req, timeout=RPC_TIMEOUT) as resp:
req = Request(url, headers={"accept": "application/dns-message", "user-agent": "darkproxy-namecoindns/1.0"}) body = resp.read()
try: result = json.loads(body.decode("utf-8"))
with urlopen(req, timeout=DOH_TIMEOUT) as resp: if result.get("error"):
if resp.status != 200: raise RuntimeError(str(result["error"]))
continue return result.get("result")
body = resp.read()
if body:
return body def lookup_namecoin_bit(label: str):
except Exception: key = f"d/{label}"
continue try:
return None 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: 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) reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, ra=1, aa=0, rcode=RCODE.NXDOMAIN), q=req.q)
return reply.pack() return reply.pack()
upstream = doh_query(payload) qname = str(req.q.qname).rstrip(".").lower()
if upstream: label = qname[: -len(".bit")].strip(".")
return upstream if not label:
return fail_response(payload) 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): def serve_udp(sock: socket.socket):
@@ -100,7 +149,7 @@ def serve_tcp(sock: socket.socket):
def main(): def main():
print(f"[namecoindns] listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True) 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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.bind((LISTEN_HOST, LISTEN_PORT)) udp_sock.bind((LISTEN_HOST, LISTEN_PORT))