diff --git a/PopuraDNS b/PopuraDNS index 7618066..f76f02b 160000 --- a/PopuraDNS +++ b/PopuraDNS @@ -1 +1 @@ -Subproject commit 7618066e2c1744e079eb7c4319e5d9308ecfcdb6 +Subproject commit f76f02bb7219fe4900755ea5c78d99259d32ea62 diff --git a/README.md b/README.md index d593d1f..c0466c4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The stack now provides DNS resolution for a comprehensive set of alternative TLD | `.onion`, `.exit`, `.onion4`, `.onion6` | Tor DNSPort (10.5.0.7:9053) | Tor hidden services | | `.i2p` | i2pd DNS (10.5.0.2:53) | I2P eepsite names | | `.eth` | ensdns (10.5.0.11) via CoreDNS | Ethereum Name Service (local resolver) | -| `.bit` | Google 8.8.8.8 | Namecoin names | +| `.bit` | namecoindns (10.5.0.12) via CoreDNS | Namecoin names (local resolver service) | | `.alt` | alt-root 185.121.177.177 | Alternative DNS root | | `.loki` | Cloudflare 1.1.1.1 | Lokinet privacy network | | `.zil` | Cloudflare 1.1.1.1 | Zilliqa blockchain | @@ -158,6 +158,24 @@ python3 scripts/resolve_ens.py ens.eth --include-text --pretty ``` +## Local Namecoin (.bit) resolver + +`.bit` lookups are routed through a local `namecoindns` service in the stack: + +* CoreDNS forwards `bit.:53` to `10.5.0.12:53`. +* `namecoindns` performs DNS-over-HTTPS upstream resolution. + +Quick test: + +```sh +docker exec darkpihole dig @10.5.0.4 A id.bit +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`). + + ## Monitoring the proxy A lightweight Prometheus/Grafana stack is included to expose 3proxy metrics diff --git a/docker-compose.yml b/docker-compose.yml index 6fea2e0..07c9721 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -239,6 +239,21 @@ services: darkproxy: ipv4_address: 10.5.0.11 + namecoindns: + build: + context: ./namecoindns + dockerfile: Dockerfile + container_name: darknamecoin + restart: unless-stopped + environment: + NAMECOIN_DOH_TIMEOUT: 6 + NAMECOIN_DOH_UPSTREAMS: https://dns.google/dns-query,https://cloudflare-dns.com/dns-query + sysctls: + - "net.ipv6.conf.all.disable_ipv6=0" + networks: + darkproxy: + ipv4_address: 10.5.0.12 + tailscale: build: context: ./tailscale diff --git a/namecoindns/Dockerfile b/namecoindns/Dockerfile new file mode 100644 index 0000000..dae38be --- /dev/null +++ b/namecoindns/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-alpine + +WORKDIR /app + +RUN pip install --no-cache-dir dnslib>=0.9.24 + +COPY server.py /app/server.py + +EXPOSE 53/udp +EXPOSE 53/tcp + +CMD ["python", "/app/server.py"] diff --git a/namecoindns/server.py b/namecoindns/server.py new file mode 100644 index 0000000..83bd724 --- /dev/null +++ b/namecoindns/server.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +import base64 +import os +import socket +import threading +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from dnslib import DNSHeader, DNSRecord, QTYPE, RCODE + + +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")) + +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() +] + + +def is_bit_query(record: DNSRecord) -> bool: + qname = str(record.q.qname).rstrip(".").lower() + return qname.endswith(".bit") + + +def doh_query(payload: bytes) -> bytes | None: + encoded = base64.urlsafe_b64encode(payload).rstrip(b"=").decode("ascii") + query = urlencode({"dns": encoded}) + + 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 + + +def fail_response(payload: bytes) -> bytes: + req = DNSRecord.parse(payload) + reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, ra=1, aa=0, rcode=RCODE.SERVFAIL), q=req.q) + return reply.pack() + + +def handle_packet(payload: bytes) -> bytes: + try: + req = DNSRecord.parse(payload) + except Exception: + return b"" + + if not is_bit_query(req): + 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) + + +def serve_udp(sock: socket.socket): + while True: + data, addr = sock.recvfrom(4096) + out = handle_packet(data) + if out: + sock.sendto(out, addr) + + +def handle_tcp_conn(conn: socket.socket): + try: + header = conn.recv(2) + if len(header) != 2: + return + length = int.from_bytes(header, "big") + payload = conn.recv(length) + if len(payload) != length: + return + out = handle_packet(payload) + conn.sendall(len(out).to_bytes(2, "big") + out) + finally: + conn.close() + + +def serve_tcp(sock: socket.socket): + while True: + conn, _ = sock.accept() + threading.Thread(target=handle_tcp_conn, args=(conn,), daemon=True).start() + + +def main(): + print(f"[namecoindns] listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True) + print(f"[namecoindns] DoH upstreams: {', '.join(DOH_UPSTREAMS)}", flush=True) + + udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp_sock.bind((LISTEN_HOST, LISTEN_PORT)) + + tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + tcp_sock.bind((LISTEN_HOST, LISTEN_PORT)) + tcp_sock.listen(128) + + threading.Thread(target=serve_udp, args=(udp_sock,), daemon=True).start() + serve_tcp(tcp_sock) + + +if __name__ == "__main__": + main()