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 |
|
| `.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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