feat(namecoin): switch .bit resolution to true on-chain via local namecoind RPC
This commit is contained in:
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user