历程：自建可编程的DNS域名解析服务

缘由

五年前，我开设了一个网站 http://ip.flares.cloud ，便利用户直接在浏览器中测试 Cloudflare 的 IP，不用下载软件。这个网站能够成立有两个关键，一个是免费以 CNAME 方式接入 Cloudflare，另一个是需要 DNS 将 a-b-c-d.ip.flares.cloud 形式的域名解析到 a.b.c.d 。

对于后者，当时采用了一个朴素的方法，在 Excel 批量生成 Zone 文件条目，然后导入到 DNS 服务中。由于 Cloudflare 的 IPv4 段很大，所以后缀统一设定为 .0 ，即便如此条目也有几千行。Zone 文件搞定了，那么上哪儿找可以上传 Zone 的 DNS 服务呢？最初，域名是在 Godaddy 上的，这个服务商支持导入 Zone（不能一口气导入，要一点一点来）。后来这里续费不再实惠，迁移到了其它域名注册商。由于网站是纯公益的项目、担心钱包遭到 “DDOS 攻击”，不敢使用任何按量付费的服务，需要寻找支持导入数千条目的免费 DNS 服务。经过一番调查，能免费做到这一点提供商很少，最终选定了 GeoScaling。

这几年下来，issue 里偶尔会收到 GeoScaling 解析出问题的消息，一般等一段时间就好了。直到今年，解析服务长时间挂掉，账号也登不上了（没有收到任何说明邮件），迫使我终于下定决心自建 DNS 域名解析服务。

Python 实现

通常自建 DNS 会用 dnsmaq 或者 bind，但这些软件也是需要枚举 Zone 的一个个条目。我想一步到位，通过程序直接实现类似于 a-b-c-d.ip.flares.cloud 到 a.b.c.d 的可编程解析。经过的调查，如果只需要 A、AAAA 解析，自己写个脚本是相对容易实现的，Python 里就有一个 pip 包 dnslib 可以接收和发送 DNS 请求。于是用 AI 起草、自己再手改，编了一个 DNS 域名解析服务器的脚本（Github）：

Copy
from dnslib import DNSRecord, QTYPE, RR, A, AAAA
from dnslib.server import DNSServer, DNSHandler, BaseResolver

class DynamicResolver(BaseResolver):
def resolve(self, request, handler):
# 获取请求的域名
qname = str(request.q.qname).lower().strip(".")
reply = request.reply()
qtype = request.q.qtype

base_domain = "flares.cloud"

# 动态解析 a-b-c-d 到 a.b.c.d
if qname.endswith(".ip." + base_domain):
parts = qname.split(".")[0].split("-")
#print(parts)
if len(parts) == 4 and all(p.isdigit() for p in parts):
# 解析为 IPv4 地址
if qtype==1:
ip = ".".join(parts)
reply.add_answer(RR(qname, QTYPE.A, rdata=A(ip), ttl=300))
else:
reply.header.rcode = 0
else:
# 如果格式不匹配，返回 NXDOMAIN
reply.header.rcode = 3 # NXDOMAIN

elif qname.endswith(base_domain):
if qtype==1:
reply.add_answer(RR(qname, QTYPE.A, rdata=A("104.21.55.142"), ttl=300))
reply.add_answer(RR(qname, QTYPE.A, rdata=A("172.67.149.32"), ttl=300))
elif qtype==28:
reply.add_answer(RR(qname, QTYPE.AAAA, rdata=AAAA("2606:4700:3034::ac43:9520"), ttl=300))
reply.add_answer(RR(qname, QTYPE.AAAA, rdata=AAAA("2606:4700:3030::6815:378e"), ttl=300))
else:
reply.header.rcode = 0

else:
# 如果不属于已定义的域，返回 NXDOMAIN
reply.header.rcode = 3 # NXDOMAIN

return reply

if __name__ == "__main__":
# 监听的地址和端口
resolver = DynamicResolver()
server = DNSServer(resolver, port=53, address="::")
print("Starting DNS server")
try:
server.start()
except KeyboardInterrupt:
server.stop()

配置与测试

选择一台开放 53 端口的服务器（不建议在中国境内这么干）。如果本身有程序占用 53 端口，关闭之（如 dnsmaq、systemd-resolved），并确保系统 DNS 查询正常工作。运行 Python 程序，执行命令如：

Copy
nslookup 104-16-96-3.ip.flares.cloud 127.0.0.1

如果一切正常，再在远程主机执行：

Copy
nslookup 104-16-96-3.ip.flares.cloud DNS服务器的IP

如果也正常，就可以尝试把域名的 NameServer 指向自己的 DNS 服务器了。例如本站是找了另一个域名建了两个子域名 ns1.ipv6.stream、ns2.ipv6.stream 指向 DNS 服务器的 IPv4/IPv6（部分域名服务商会检查 NS 域名 A 解析的 IP 地址是否为 DNS，如果没有会设置失败）。在域名服务商设置好以后，等待一段时间就行了。如果不确定，可以执行以下命令检查：

Copy
dig +trace 104-16-96-3.ip.flares.cloud

可以看到从根→cloud 顶级域→自建 DNS→结果的解析过程是畅通的：

Copy
;; global options: +cmd
. 87203 IN NS k.root-servers.net.
. 87203 IN NS a.root-servers.net.
. 87203 IN NS d.root-servers.net.
. 87203 IN NS h.root-servers.net.
. 87203 IN NS j.root-servers.net.
. 87203 IN NS e.root-servers.net.
. 87203 IN NS c.root-servers.net.
. 87203 IN NS i.root-servers.net.
. 87203 IN NS f.root-servers.net.
. 87203 IN NS l.root-servers.net.
. 87203 IN NS g.root-servers.net.
. 87203 IN NS b.root-servers.net.
. 87203 IN NS m.root-servers.net.
. 87203 IN RRSIG NS 8 0 518400 20250124050000 20250111040000 26470 . R1Mbylc4QrkWbTyoX5X/fM7XkGSlaovOqYSyQ1ZuujRL+pV3ekI9ytoz 9X3LS6Hm7T40XeO/Z6d2pb3yryqYoXeAY0meJHdEfZwYT3/7AIUuWu4r eKt/K/G0bL5VM26qHgj6aTYheHIh2M8dXtpx43o0hHuM87X2hnReLNbX V8+gc8cjbpmATREbzU4db70gckL9NuZtleQdpbRUcwJcakudwXwIST1S X3wnIWMo0R52A4hfcODsb3tL3LS0drJuceIvhMZSNwubAowfWt7CzMjd MkEYaErsIfJMrbJ8AXMcb9wZjsPUBzUqyLm7Smt5/LvxtPzJLSQRNr2B a6K4+Q==
;; Received 525 bytes from 8.8.8.8#53(8.8.8.8) in 4 ms

cloud. 172800 IN NS ns01.trs-dns.com.
cloud. 172800 IN NS ns01.trs-dns.net.
cloud. 172800 IN NS ns01.trs-dns.org.
cloud. 172800 IN NS ns01.trs-dns.info.
cloud. 86400 IN DS 7041 13 2 03E1B41319FDD3A14E27CE35FFBA8036DA51E328165CE102AE69A6C1 203F64E4
cloud. 86400 IN RRSIG DS 8 1 86400 20250124050000 20250111040000 26470 . PB/tyNRHUg9+Fpg4tAweaHLFSwvam2nZxTnwikFfmRPTc9121azZ8WRX r8/N92OKcPBpISyi4dONjyFa5n60xDJDg5SwW68cnE435Dpa2vCqqECC 5rkOMTj8WliC2Coyt8cdEHDKYA4IACWAxkKCG6GLY6FKuMUUIeF1eQwt 4QIWg5ac8UKR0/exlgpChNPPfgOE6f7NKl7tXnx9s9P38MgVGM531BAl j0ES74lQaHkQBF7nTP8kSyu2A3Z5Fh4Ly3lWbhzJwzZuiuh90ybStCU3 cPt/gxaEpfq/PC79RgHxo2db61i4PsJmxT2a0IlgkF9nApJ9gqgwV9GB Uj7txw==
;; Received 693 bytes from 2001:7fe::53#53(i.root-servers.net) in 8 ms

flares.cloud. 900 IN NS ns1.ipv6.stream.
flares.cloud. 900 IN NS ns2.ipv6.stream.
HT3BBE52QFEA3EKHEL6VAE4CVUE8777V.cloud. 300 IN NSEC3 1 1 0 - HT6L5OLG1P6S8KJ46GSSFH8NK8V88KN8 NS SOA RRSIG DNSKEY NSEC3PARAM
HT3BBE52QFEA3EKHEL6VAE4CVUE8777V.cloud. 300 IN RRSIG NSEC3 13 2 300 20250210064627 20250111064627 36804 cloud. f7tgopfGAr1qRF2uaDshi25DHDi+tYkRe4Il/6OQziUQ8v4ykr8vou2Z WWYiRqhPuk170ftGZhLJEiyCVRIkuQ==
U5U5ADCUK0HTP2QQ80FQH8566IQU6VLU.cloud. 300 IN NSEC3 1 1 0 - U60GR3IIOBUJHOBIHNJQISHOGCPPUO4E NS DS RRSIG
U5U5ADCUK0HTP2QQ80FQH8566IQU6VLU.cloud. 300 IN RRSIG NSEC3 13 2 300 20250210064627 20250111064627 36804 cloud. sjsXE16m+Qlew47+Ifrvq9L2C/Mq/HkPelt5l2Vd+UCi3U0Vyd4mddlH LTiR+QqsW0ZxToZfIj2VS6w9z43IBg==
;; Received 504 bytes from 2620:57:4001::1#53(ns01.trs-dns.com) in 8 ms

;; expected opt record in response
104-16-96-3.ip.flares.cloud. 300 IN A 104.16.96.3
;; Received 61 bytes from 148.135.6.152#53(ns1.ipv6.stream) in 4 ms

以服务形式运行

如果一切都没问题了，可以写个 systemctl 服务 /etc/systemd/system/dns-server.service 保持脚本运行：

Copy
[Unit]
Description=Custom DNS Server for ip.flares.cloud
After=network.target

[Service]
ExecStart=/usr/bin/python3 /root/dns-server/dns_server.py
WorkingDirectory=/root/
StandardOutput=journal
StandardError=journal
Restart=always
User=root
Group=root
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target

执行以下命令启动：

Copy
sudo systemctl daemon-reload
sudo systemctl restart dns-server.service

查阅日志：

Copy
journalctl -u dns-server.service -f

以 Docker 形式运行

当然，也可以封装到 Docker 执行。在同一个文件夹建立 Dockerfile：

Copy
FROM python:3.9

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD [ "python", "./dns_server.py" ]

建立 Docker 并执行：

Copy
docker build -t dns_server .
docker run --network=host --restart unless-stopped --name=dns_server -e PYTHONUNBUFFERED=1 dns_server

说明

需要强调，以上实现非常简单，只有最基本的功能，行为未必规范。代码没有经过任何安全审计，请只在无重要数据、可以随便重建的虚拟机中进行研究。

顺便一提，虽然指定任意后缀乃至于解析 IPv6 已无障碍，但当前各 IPv6 段和相同 /24 IPv4 段的网络情况比较接近，分开测试意义不大。因此暂时不会做到网站里，如果未来有明确需求再做改动。