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`.
|
* 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
@@ -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
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user