5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

皆さんはDNSに触れたことがあるだろうか?
10人中9人がはいと答えて、10人目が嘘をついていることはすぐに分かる1
今この記事を見ている間にも、あなたのコンピューターは常にDNSとのやり取りを続け、この記事をクリックしたときもDNSは動いたはずだ。たまにはそんなDNSと戯れてみようではないか。

そもそもDNSとはなんであるか

あなたがqiita.comというリンクをクリックしたとき、コンピュターはまず相手がどこにいるのかを探す。
相手のサーバーを見つけないことには何もすることはできない。相手の住所、つまりip addressが必要になる。それを助けてくれるのがDNSである。
以下はあなたがWebページを開く際にDNSが果たす役割である。
スクリーンショット 2025-12-24 13.43.15.png
詳しくは上の図を見てほしいが、要約すれば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から変更できる。
スクリーンショット 2025-12-24 14.29.38.png

キャッシュしてみる

今回は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です。

  1. 2009年センター英語の言い回し

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?