feat(dns): add local namecoindns service and route .bit locally
This commit is contained in:
+1
-1
Submodule PopuraDNS updated: 7618066e2c...f76f02bb72
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user