任意のプラベートIPを返すような動的なDNS権威サーバが欲しくなったので調べたところ github.com/miekg/dns というライブラリでサクッとかけそうだったので書いてみた。
net/http を模したライブラリで、DNSサーバを簡単に書くことができる。
package main import ( "log" "net" "github.com/miekg/dns" ) func main() { dns.HandleFunc("example.com", func(w dns.ResponseWriter, r *dns.Msg) { msg := &dns.Msg{} msg.SetReply(r) msg.Authoritative = true for _, q := range msg.Question { if q.Name == "hello.example.com." && q.Qtype == dns.TypeA { rr := &dns.A{ Hdr: dns.RR_Header{Name: q.Name, Rrtype: q.Qtype, Class: q.Qclass, Ttl: 300}, A: net.ParseIP("127.0.0.1"), } msg.Answer = append(msg.Answer, rr) } } w.WriteMsg(msg) }) s := &dns.Server{Addr: ":53", Net: "udp"} err := s.ListenAndServe() if err != nil { log.Fatal(err) } }
$ dig @127.0.0.1 +short hello.example.com 127.0.0.1
ゾーンファイルのパースもできる。
func main() { buf := strings.NewReader(` www.example.com. 300 IN A 127.0.0.1 www2.example.com. 300 IN A 127.0.0.2 `) z := dns.NewZoneParser(buf, "", "") for rr, ok := z.Next(); ok; rr, ok = z.Next() { fmt.Println(rr.String()) } fmt.Println(z.Err()) }
$ go run main.go www.example.com. 300 IN A 127.0.0.1 www2.example.com. 300 IN A 127.0.0.2 <nil>
作ったDNSサーバがこちら。
$ kodama example.com & [1] 25270 $ dig @127.0.0.1 +short 127-0-0-1.example.com 127.0.0.1 $ dig @127.0.0.1 +short web-192-168-10-1.example.com 192.168.10.1
fly.ioでDNSサーバを動かす
作ったDNSサーバをどこかで動かしたい…と思っていたらなんと fly.io がUDP/TCPに対応していた。
IPv4をアロケートして、以下のような fly.toml でアプリをデプロイしたら普通に動いた。
app = 'kodama' primary_region = 'nrt' [build] image = 'ghcr.io/winebarrel/kodama:v1.0.0' [env] KODAMA_DOMAIN = 'foo.example.com' KODAMA_ZONE_DATA = """ ; ... bar.foo.example.com. 3600 IN NS xxx.xxx.xxx.xxx """ [[services]] protocol = 'udp' internal_port = 53 [[services.ports]] port = 53 [[services]] protocol = 'tcp' internal_port = 53 [[services.ports]] port = 53 [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1

おまけ: Google Public DNSの挙動
せっかくなので動かしたDNSサーバでLet's Encryptの証明書を取得しようとしてみた。 以下のようなDNSチャレンジ用のTXTレコードを設定してcertbotを動かしたが、なぜかチャレンジに失敗する。
_acme-challenge.foo.example.com. 900 IN TXT xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
手元のdigでは値が返るのになぜ…と思ってログを仕込んでみたら、@8.8.8.8 @8.8.4.4 のリゾルバからは大文字小文字がランダムになったホスト名が問い合わせされてきた。
2025-12-XXTXX:XX:XXZ app[...] nrt [info]2025-12-XXTXX:XX:XX DEBUG NOT FOUND question=";_AcMe-chALlenGE.foo.eXamPle.com.\tIN\t A"
それで大文字の問い合わせに対応していないバグに気がついたが、なんでそうなっているのが調べたところセキュリティ対策とのこと。
キャッシュポイズニングで不正なパケットが返されたときに、問い合わせのホスト名と応答のホスト名が違っていれば、キャッシュが汚染されたことがわかる、ということらしい。
ちなみに 1.1.1.1 はそのような挙動にはなっていなかった。