理由#
五年前、私は http://ip.flares.cloud というウェブサイトを開設しました。これは、ユーザーがソフトウェアをダウンロードすることなく、ブラウザで直接 Cloudflare の IP をテストできる便利なサイトです。このサイトが成立するためには、2 つの重要な要素があります。一つは、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 には DNS リクエストを受信および送信できる pip パッケージ dnslib があります。そこで、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("DNSサーバーを起動中")
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 サーバーに向けることができます。例えば、当サイトは別のドメインを使って 2 つのサブドメイン ns1.ipv6.stream
、ns2.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 セグメントのネットワーク状況は非常に似ているため、分けてテストする意義はあまりありません。したがって、当面はウェブサイトに組み込む予定はなく、将来的に明確な需要があれば変更を行う予定です。