c2a / scripts /cloudflared_doh_dns.py
Codex
部署新版 C2A 应用
56e59a8
Raw History Blame Contribute Delete
3.79 kB
"""Tiny local DNS forwarder for cloudflared when FlClash Fake-IP DNS is enabled.
It resolves A/AAAA records through Cloudflare DoH over the configured HTTP proxy,
then returns a normal DNS response on loopback. It deliberately listens only on
127.0.0.1 and does not persist credentials or query data.
"""
from __future__ import annotations
import ipaddress
import json
import os
import socket
import struct
import sys
from urllib.parse import urlencode
from urllib.request import ProxyHandler, Request, build_opener
LISTEN_HOST = "127.0.0.1"
LISTEN_PORT = int(os.environ.get("DOH_DNS_PORT", "53535"))
DOH_URL = "https://cloudflare-dns.com/dns-query"
LOG_PATH = os.environ.get("DOH_DNS_LOG", "D:/work/code/ai-ecology/c2a/data/cloudflared-doh-dns.log")
def log(message: str) -> None:
try:
with open(LOG_PATH, "a", encoding="utf-8") as stream:
stream.write(message + "\n")
except OSError:
pass
def read_question(packet: bytes) -> tuple[str, int, int, int]:
offset = 12
labels: list[str] = []
while offset < len(packet):
length = packet[offset]
offset += 1
if length == 0:
break
if length & 0xC0:
raise ValueError("compressed question name is unsupported")
labels.append(packet[offset : offset + length].decode("ascii"))
offset += length
qname = ".".join(labels) + "."
qtype, qclass = struct.unpack("!HH", packet[offset : offset + 4])
return qname, qtype, qclass, offset + 4
def doh_lookup(name: str, qtype: int) -> list[tuple[int, str, int]]:
query = urlencode({"name": name, "type": "A" if qtype == 1 else "AAAA"})
request = Request(
f"{DOH_URL}?{query}",
headers={"Accept": "application/dns-json", "User-Agent": "c2a-cloudflared-dns/1"},
)
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") or "http://127.0.0.1:7890"
opener = build_opener(ProxyHandler({"https": proxy, "http": proxy}) if proxy else ProxyHandler({}))
with opener.open(request, timeout=15) as response:
payload = json.load(response)
records: list[tuple[int, str, int]] = []
for answer in payload.get("Answer") or []:
if int(answer.get("type") or 0) == qtype:
records.append((qtype, str(answer.get("data") or ""), int(answer.get("TTL") or 60)))
return records
def build_response(query: bytes) -> bytes:
txid = query[:2]
qname, qtype, qclass, question_end = read_question(query)
question = query[12:question_end]
records: list[tuple[int, str, int]] = []
if qclass == 1 and qtype in (1, 28):
records = doh_lookup(qname, qtype)
log(f"query name={qname} type={qtype} records={[item[1] for item in records]}")
answers = bytearray()
for record_type, value, ttl in records:
try:
packed = ipaddress.ip_address(value).packed
except ValueError:
continue
answers.extend(struct.pack("!HHHIH", 0xC00C, record_type, 1, max(1, ttl), len(packed)))
answers.extend(packed)
flags = 0x8180 if records else 0x8183
header = txid + struct.pack("!HHHHH", flags, 1, len(records), 0, 0)
return header + question + bytes(answers)
def main() -> int:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((LISTEN_HOST, LISTEN_PORT))
print(f"DNS forwarder listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True)
while True:
packet, address = sock.recvfrom(4096)
try:
response = build_response(packet)
except Exception:
log("query failed")
response = packet[:2] + struct.pack("!HHHHH", 0x8182, 1, 0, 0, 0) + packet[12:]
sock.sendto(response, address)
if __name__ == "__main__":
raise SystemExit(main())