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 段的網絡情況比較接近,分開測試意義不大。因此暫時不會做到網站裡,如果未來有明確需求再做改動。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。