TulvL

TulvL

twitter
github

历程:自建可编程的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.clouda.b.c.d 的可编程解析。经过的调查,如果只需要 A、AAAA 解析,自己写个脚本是相对容易实现的,Python 里就有一个 pip 包 dnslib 可以接收和发送 DNS 请求。于是用 AI 起草、自己再手改,编了一个 DNS 域名解析服务器的脚本(Github):

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 程序,执行命令如:

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

如果一切正常,再在远程主机执行:

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

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

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

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

;; 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 保持脚本运行:

[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

执行以下命令启动:

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

查阅日志:

journalctl -u dns-server.service -f

以 Docker 形式运行#

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

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 并执行:

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

说明#

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

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

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。