feat(dns): add local namecoindns service and route .bit locally
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:15:30 -05:00
parent bde8a27cde
commit 958947b334
5 changed files with 165 additions and 2 deletions
Submodule PopuraDNS updated: 7618066e2c...f76f02bb72
+19 -1
View File
@@ -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 | | `.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 | | `.i2p` | i2pd DNS (10.5.0.2:53) | I2P eepsite names |
| `.eth` | ensdns (10.5.0.11) via CoreDNS | Ethereum Name Service (local resolver) | | `.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 | | `.alt` | alt-root 185.121.177.177 | Alternative DNS root |
| `.loki` | Cloudflare 1.1.1.1 | Lokinet privacy network | | `.loki` | Cloudflare 1.1.1.1 | Lokinet privacy network |
| `.zil` | Cloudflare 1.1.1.1 | Zilliqa blockchain | | `.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 ## Monitoring the proxy
A lightweight Prometheus/Grafana stack is included to expose 3proxy metrics A lightweight Prometheus/Grafana stack is included to expose 3proxy metrics
+15
View File
@@ -239,6 +239,21 @@ services:
darkproxy: darkproxy:
ipv4_address: 10.5.0.11 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: tailscale:
build: build:
context: ./tailscale context: ./tailscale
+12
View File
@@ -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"]
+118
View File
@@ -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()