はじめに
皆さんはDNSに触れたことがあるだろうか?
10人中9人がはいと答えて、10人目が嘘をついていることはすぐに分かる1。
今この記事を見ている間にも、あなたのコンピューターは常にDNSとのやり取りを続け、この記事をクリックしたときもDNSは動いたはずだ。たまにはそんなDNSと戯れてみようではないか。
そもそもDNSとはなんであるか
あなたがqiita.comというリンクをクリックしたとき、コンピュターはまず相手がどこにいるのかを探す。
相手のサーバーを見つけないことには何もすることはできない。相手の住所、つまりip addressが必要になる。それを助けてくれるのがDNSである。
以下はあなたがWebページを開く際にDNSが果たす役割である。

詳しくは上の図を見てほしいが、要約すればDNSはあなたから受け取ったドメインを元にip addressを返すものである。
ここで考えてみよう、もしあなたがDNSをコントロールできれば何ができるだろうか?
- 自分のコンピューターにキャッシュすれば、より高速にWebページにアクセスできる
- アクセスしたサイトや使ったアプリなどをDNSから分析できる
......
つまりDNSで結構色々なことができるのである
GoでDNSサーバーを立ててみる
今回は簡単のためmiekg/dnsを使う
package main
import (
"fmt"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
func resolve(domain string, qtype uint16) []dns.RR {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), qtype)
m.RecursionDesired = true
c := new(dns.Client)
in, _, err := c.Exchange(m, "1.1.1.1:53")
if err != nil {
return []dns.RR{}
}
_ = cacheResolve(domain, in.Answer)
return in.Answer
}
type dnsHandler struct{}
func (h *dnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
msg := new(dns.Msg)
msg.SetReply(r)
msg.Authoritative = true
for _, question := range r.Question {
fmt.Printf("%s\n", question.Name)
answers := resolve(question.Name, question.Qtype)
msg.Answer = append(msg.Answer, answers...)
}
w.WriteMsg(msg)
}
func main() {
handler := new(dnsHandler)
server := &dns.Server{
Addr: ":53",
Net: "udp",
Handler: handler,
UDPSize: 65535,
ReusePort: true,
}
fmt.Println("Starting DNS server on port 53")
err := server.ListenAndServe()
if err != nil {
fmt.Printf("Failed to start server: %s\n", err.Error())
}
}
さて、あとはコンピューターのネットワーク設定からこのDNSを使うように設定すれば基本的には終わりである
設定する
macの場合はシステム設定->ネットワーク->詳細->DNSから変更できる。

キャッシュしてみる
今回はredisを使ってキャッシュを管理する
var Ctx = context.Background()
var DB = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6380",
Password: "",
DB: 0,
})
func PushExp(key string, value string, exp time.Duration) error {
err := DB.Set(Ctx, key, value, exp).Err()
return err
}
func Get(key string) (string, error) {
v, err := DB.Get(Ctx, key).Result()
return v, err
}
func stringToDnsRR(str string) []dns.RR {
rrStrs := strings.Split(str, ",")
var rrs []dns.RR
for _, srr := range rrStrs {
if srr == "" {
continue
}
rr, err := dns.NewRR(srr)
if err != nil {
continue
}
rrs = append(rrs, rr)
}
return rrs
}
func dnsRRToString(rrs []dns.RR) string {
var rrStrs string
for _, rr := range rrs {
rrStrs += fmt.Sprintf("%v,", rr.String())
}
return rrStrs
}
func getCacheResolve(domain string) []dns.RR {
srr, err := redis.Get(domain)
if err != nil {
return nil
}
return stringToDnsRR(srr)
}
func cacheResolve(domain string, rrs []dns.RR) error {
err := redis.PushExp(domain, dnsRRToString(rrs), time.Hour)
return err
}
import (
"fmt"
"net"
"time"
"strings"
+ "github.com/miekg/dns"
+ "github.com/redis/go-redis/v9"
)
func resolve(domain string, qtype uint16) []dns.RR {
+ if cache := getCacheResolve(domain); cache != nil {
+ return cache
+ }
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(domain), qtype)
m.RecursionDesired = true
c := new(dns.Client)
in, _, err := c.Exchange(m, "1.1.1.1:53")
if err != nil {
return []dns.RR{}
}
+ _ = cacheResolve(domain, in.Answer)
return in.Answer
}
少々面倒ではあるが、これでアクセス速度は早くなる。
参考までに測定結果は以下のようになった。
| Go + キャッシュあり | ルーターのDNS | |
|---|---|---|
| 初回アクセス10回平均 | 28ms | 74ms |
| 二回目アクセス10回平均 | 0.6ms | 43ms |
これを見ると明らかに速度が違うことが見て取れるだろう。
極単純に考えると、最大で42msも早くWebページを開くことができる計算になる。
履歴を保存してみる
var DB *sql.DB = NewClient()
func Execute(query string) (any, error) {
res, err := DB.Exec(query)
return res, err
}
func NewClient() *sql.DB {
db, err := sql.Open("sqlite3", "app.db")
if err != nil {
panic("Could not open database")
}
return db
}
func pushHistory(domain string, rrs []dns.RR) {
_, _ = sqlite3.DB.Exec("INSERT INTO log ( domain, resolve ) VALUES ( ?, ? )", domain, dnsRRToString(rrs))
}
import (
"fmt"
"net"
"time"
"strings"
+ "database/sql"
"github.com/miekg/dns"
"github.com/redis/go-redis/v9"
+ _ "github.com/mattn/go-sqlite3"
)
func (h *dnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
msg := new(dns.Msg)
msg.SetReply(r)
msg.Authoritative = true
for _, question := range r.Question {
fmt.Printf("%s\n", question.Name)
answers := resolve(question.Name, question.Qtype)
+ go func() {
+ pushHistory(question.Name, answers)
+ }()
msg.Answer = append(msg.Answer, answers...)
}
w.WriteMsg(msg)
}
create table log
(
id INTEGER
primary key autoincrement,
domain VARCHAR(4096),
resolve TEXT,
create_at DATETIME default CURRENT_TIMESTAMP
);
ちょっと分析してみる
ある時間にどのサービスを最も使ったかを分析してみる
WITH HourlyCounts AS (
-- 時間とドメインごとにカウントする
SELECT
strftime('%Y-%m-%d %H:00:00', create_at) AS hour_slot,
domain,
COUNT(*) AS access_count
FROM log
GROUP BY hour_slot, domain
),
RankedDomains AS (
-- 各時間帯の中で多い順に順位をつける
SELECT
hour_slot,
domain,
access_count,
RANK() OVER (PARTITION BY hour_slot ORDER BY access_count DESC) as rnk
FROM HourlyCounts
)
SELECT
hour_slot,
domain,
access_count,
rnk
FROM RankedDomains
WHERE rnk = 1
ORDER BY hour_slot DESC;
| hour_slot | domain | access_count |
|---|---|---|
| 2025-12-25 09:00:00 | www[.]notion.so. | 74 |
| 2025-12-25 08:00:00 | www[.]youtube.com. | 203 |
| 2025-12-25 07:00:00 | www[.]youtube.com. | 189 |
終わりに
如何だっただろうか。みなさんも良きDNSライフを送れることを願ってやまない。
余談
これを書き終わったあとにまず変な口調で書き始めたことを恨み、そして忙しい中でそこまで重要じゃないそもそもDNSとは何であるかの図の作成に1時間かけたことに少しの後悔をしました。ちなみにツールはGoogle Slideです。
-
2009年センター英語の言い回し ↩