システムデザイン2026年6月13日1 分で読了

10年間、盲目的に信じてきたものを理解するために、DNSサーバーを自作した

10年間、盲目的に信じてきたものを理解するために、DNSサーバーを自作した

DNS以前:インターネット全体が、たった一つのテキストファイルで動いていた

DNSなぜ存在するのかを理解するには、それが存在しなかった時代まで遡らなければならない——そして正直に言うと、この物語に私は夢中になった。

1980年代初頭、ARPANET(インターネットの祖先)にDNSはなかった。ではコンピューターはどうやって互いを見つけていたのか?テキストファイルでだ。そう——HOSTS.TXTという、どのマシン名がどのIPに対応するかを並べた一つのファイル。カリフォルニアのスタンフォード研究所(SRI-NIC)が管理していた。

その仕組みは、今思えば滑稽なほどだ。新しいマシンを手に入れてネットに繋ぎたければ……SRIに電話かメールをする。SRIが手作業でファイルを更新する。そして一日に一度、ネットワーク上の全マシンがそのファイルを再ダウンロードする。「インターネットの電話帳」全体が、手で編集され、日に一度配送される、たった一つの文書だったのだ。

問題はすぐ分かるだろう?数百台なら問題ない。だがネットワークは指数関数的に成長していた。ファイルは膨れ上がり、それをダウンロードするだけのトラフィックも膨れ上がり、そして何より——たった一つの組織が、世界中のあらゆる変更を手作業で承認しなければならなかった。 地球の両端にある二台のマシンは同じ名前を持てない。電話帳が共有だからだ。それはボトルネックであり、首を絞めつつあった。

1983年、ポール・モカペトリスという人物が——伝説的なジョン・ポステルの下、USCで働いていた——たった一つの仕事を任された。これを置き換える何かを考え出せ、と。彼は二つの文書、RFC 882とRFC 883を書き、Domain Name Systemが生まれた。

天才的だったのは「名前を数字に変換する」ことではない——HOSTS.TXTがすでにやっていた。天才的だったのは二つのアイデアだ。階層(hierarchy)と委任(delegation)。一人が抱える巨大な電話帳の代わりに、それを木にする。ルートは誰が.com.orgを管理するかだけ知っていればいい。.comを管理する者は誰がexample.comを管理するかだけ知っていればいい。そしてexample.comの中身の詳細は?その実際の持ち主に任せればいい。各枝が自分の部分だけを管理し、誰も他人の分を承認しなくていい。

この逆転——中央集権から分散へ——こそ、DNSが40年生き延び、今や一日に数兆のクエリを運ぶ理由であり、HOSTS.TXTがとうに死んだ理由だ。そしてmini-dnsを作りながら、私は気づいた。自分はまさにあの1983年の哲学を再構築しているのだと。自分の部分を知り、自分のものでない部分には道を指し示すサーバーを。

DNSの実際の動き:一つの問いはいくつの扉をくぐるのか?

あの階層の哲学を、動くメカニズムに翻訳するとこうなる。www.example.comと打つと、あなたの背後で数十ミリ秒のうちにドミノが倒れていく。

  1. ブラウザがあなたのマシンに尋ねる。あなたのマシン(スタブリゾルバ)が**再帰リゾルバ(recursive resolver)**に尋ねる——たいていISPのもの、あるいは8.8.8.81.1.1.1

  2. 再帰リゾルバが知らなければ、特定の誰かに尋ねるのではなく——ルートから木を登る。まずルートサーバー.)に尋ねる。「.comという末尾は誰が管理している?」

  3. ルートは答える。「IPは知らないが、.comを管理しているこのTLDサーバーに聞け」。→これはリファラル(referral)、答えではなく、道案内だ。

  4. 再帰リゾルバは次に.comのTLDに尋ねる。「example.comは誰が管理している?」→TLDはまた、そのドメインの**権威サーバー(authoritative server)**へと指し示す。

  5. 最後に再帰リゾルバは権威サーバー——答えを本当に所有している者——に尋ねる。「www.example.comのIPは?」→それが答える。192.0.2.1

5ステップ、1回のアクセスのために。あなたがページを開くたびにこの遅延を感じないのは、各層のキャッシュのおかげだ——システム全体の魂であり、後で詳しく語る。

私が「おお」と漏らしたこと:どのサーバーもすべてを知らない。 これがまさに1983年の遺産だ——各層は「次に聞くべき者は誰か」だけを知っている。中央集権を、その遺伝子レベルで拒む設計。

mini-dnsはこの劇で二役を演じる。権威サーバー(自分で定義したゾーンを所有)であると同時に、再帰フォワーディング(未知の名前を上流へ押し上げ、覚えておく)もこなす。

DNSの応答は、パケットの中でどう見えるのか?

サーバーに応答させるには、一バイトずつ自分で組み立てねばならない。DNSメッセージは四つの部分からなる。

  • ヘッダー:トランザクションID(問いと答えを対応づけるため)と、極めて重要なフラグ群を持つ。

  • 質問(Question):何を尋ねているか——ドメイン名、レコード種別、クラス。

  • 回答 / 権威 / 追加(Answer / Authority / Additional):答えと補足情報を持つ三つの区画。

最も多くを教えてくれたのはフラグだ。

  • AA(Authoritative Answer):立っていれば「この答えはキャッシュではなく本当の持ち主から来た」という意味。mini-dnsが自分のゾーン内の名前に答えるときはこのフラグを立て、転送データを返すときは立てない。「確かに知っている」と「又聞きだ」を区別する——DNSにはそれが組み込まれている。

  • RCODE:結果コード。NOERRORなら正常。NXDOMAINは「この名前はこの世に存在しない」。そしてここに巧妙な罠がある——NXDOMAINNODATAは違う

その罠に、私は初回で引っかかった。

  • NXDOMAIN = 名前がそもそも存在しない。例えばnantokanai.example.com

  • NODATA = 名前は存在するが、あなたが尋ねた種別のレコードがない。例えばexample.comAレコード(IPv4)を持つが、あなたがそれにAAAA(IPv6)を尋ねた——名前はある、その種別は空だ。このときは空の回答区画とともにNOERRORを返さねばならず、NXDOMAINではない

些細に聞こえるが、この二つを取り違えるのが「メールが突然送れない」「クライアントが延々とリトライする」といったバグの真の原因だ。DNSは「あるかないか」だけを答えるのではない——「ない、ではどういう種類のないか」を答える。 そしてその「どういう種類か」が致命的に重要なのだ。

ゾーンファイル:権威サーバーの、剥き出しの心臓

私が立ち止まった事実:ドメインの元データはただのテキストファイルだ。そう——HOSTS.TXTから40年経っても、心臓はやはりテキストなのだ。違いは、いまや誰もが自分の部分だけを持つこと。

example.com.     3600 SOA   ns1.example.com. admin.example.com. 1 7200 3600 1209600 3600
example.com.     3600 NS    ns1.example.com.
example.com.     3600 A     192.0.2.1
example.com.     3600 AAAA  2001:db8::1
www.example.com. 3600 CNAME example.com.
example.com.     3600 MX    10 mail.example.com.
example.com.     3600 TXT   "v=spf1 -all"
*.example.com.   3600 A     192.0.2.9

各レコード種別はパズルのピースであり、一つずつ自分で扱ったときに初めて本当に理解できた。

  • SOA(Start of Authority)——ゾーンの「出生証明書」の行。後ろの数字は飾りではない。serial(ゾーンのバージョン。セカンダリサーバーが変更を察知して同期するため)、続いてrefreshretryexpire、そして最も重要なのが——最後の数字はネガティブキャッシュのTTL、つまり「NXDOMAINエラーをどれだけ覚えておくか」だ。システム全体の負荷耐性を決める、ちっぽけな数字。

  • NS——誰が権威ネームサーバーかを示す。これがあなたのゾーンをグローバルな階層に結びつける糸だ——1983年にモカペトリスが描いた、まさにあの木に。

  • A / AAAA——名前をIPv4 / IPv6アドレスに向ける。

  • CNAME——別名。厳格なルール:CNAMEを持つ名前は、同じ階層に他のレコードを持てない。解決時、wwwexample.comを指すCNAMEなら、まともなサーバーはそのまま辿り(CNAMEチェイニング)、宛先を同じ応答に詰め込んで、クライアントがもう一往復尋ねずに済むようにする。

  • MX——メールの行き先、優先度の数字(10)付き。この行を間違えると会社中のメールが虚空に落ちる。

  • TXT——なんでも入る箱:メールなりすまし対策のSPF、ドメイン所有権の確認、DKIM……

  • * ワイルドカード——宣言されていない全サブドメインを捕まえる。便利だが諸刃の剣:タイプミス一つで、千の幽霊サブドメインが間違った場所を指す。

パーサーを書きながら、この職業について多くを物語る小さな決断に直面した。一行が間違って打たれていたら——末尾のドット欠け、構文ミス——どうする?ファイル全体を落とすか?その行を飛ばすか?

私は選んだ。間違った行番号を正確にログに出し、それを飛ばし、残りを生かす。それが「ドット一つの欠け」と「真夜中に会社中がウェブサイトを失う」との境界線だ。本番障害の大半は、大きなエラーから来ない。手当てを欠いた、ちっぽけな何かから来る。

(ところで、example.com.の末尾のドットに気づいた?あれはFQDN——「これは絶対名だ、すでにルートまで来ている、何も付け足すな」という意味だ。それがないと、多くのシステムはドメインを付け足してexample.com.example.comにしてしまう。ドット一つ。デバッグに一晩。)

TTLとキャッシュ——DNSの魂であり、そして痛みでもある

各行の3600TTLTime To Live)、秒単位だ。「この答えは1時間覚えておいてよい」。

これが業界で最も苦痛な呪文の犯人だ。DNSを変えたのに、なぜ反映されない?」

以前は伝播とはインターネットが「怠けている」のだと思っていた。違う。インターネットは遅いのではない——あなた自身の約束を守っているのだ。TTL 3600と設定したとき、あなたは世界中に「これは1時間覚えていていい」と告げた。そして道中の全リゾルバが覚えた。今あなたが10分後に気を変えても……彼らは、カウントダウンがゼロになるまで、あなたの古い約束を守り続けている。

そしてこれはキャッシュを自分で書いて初めて理解した。TTLは一つの時計ではない。各層で独立にカウントダウンする。 ブラウザのキャッシュ、OSのキャッシュ、ISPリゾルバのキャッシュ——それぞれが自分の時計で一部を保持する。だから自宅ではサイトが上がって見えるのに、別のネットワークにいる友人はまだ落ちていると報告する。誰も間違っていない——彼らの時計がまだゼロになっていないだけだ。

私が引き出した、エンジニアリングをはるかに超えて通用する要点:キャッシュは速度と真実の間のトレードオフだ。 長く覚えれば速くなるが、真実が変わったとき間違える確率も上がる。あらゆるシステムは——正直に言えば人間も——そのトレードオフの中に生きている。

血で覚えた実戦の知恵:重要なレコードを変える予定があるなら、その数日前にTTLをぐっと下げておけ。 そうすれば本番で変えたとき、世界が古い値を速く忘れる。誰も教えてくれない。午前2時に伝播を待ったことのある者だけが、骨に刻んでいる。

最も深い教訓:存在しないものこそキャッシュせねばならない

これが、今も考え続けている「あっ」の瞬間だ。

正しい答えをキャッシュするのは当然だ。google.comを一度尋ね、覚え、次は即答する。だが、はるかに巧妙なものがある。ネガティブキャッシュ(negative caching)——「この名前は存在しない」という結果(NXDOMAIN)すら覚えておくこと。

最初は不条理に聞こえた。存在しないものを覚えて何になる?

そして理解した。外には盲滅法に探りを入れる無数のボットがいる。aaa.example.comadmin123.example.comxyz.example.com……ゴミに当たるたびにサーバーが律儀に木を登って尋ね、「ない」と返ってくるなら、それは自分を犠牲者に変えてしまう。「こいつは存在しない」と覚えること——そしてどれだけの間覚えるかは、まさに上のSOAの行が定める——それが盾だ。(ほら、すべてが繋がっている。)

これは奇妙に美しく、エンジニアリングを超えて届く。何かが存在しないと知ることもまた知識であり、時に最も価値ある種類のものだ。 どの道がどこにも通じないかを知ること。どのやり方が効かないかを知ること。賢いシステムは正しい答えだけを覚えるのではない——行き止まりも覚えて、二度と歩かない。成熟した人間も同じだ。

私はさらに**シングルフライト(single-flight)**を加えた。キャッシュが空のとき、同一の問いが百個同時に殺到したら、尋ねに行けるのは一つだけ。残りの九十九は待って結果を分け合う。これがないと、突然のトラフィックの奔流があなたのサーバーをポンプに変え、頼っているまさにその上流を、知らぬ間に攻撃しかねない。圧力下でシステムができる最善は、時に自制すること——同じ問いを九十九回繰り返さないことなのだ。

UDP、512バイト、そしてDNSがTCPと持つ奇妙な「継ぎ手」

誰もが「DNSUDPの53番ポートで動く」と暗記する。本当だ、だが半分が抜けている。

UDPは速く、軽く、面倒なハンドシェイクがない——電光石火の一問一答に最適だ。だが従来のUDP-DNS512バイトで頭打ちになる(ネットワークが脆弱だった1983年に選ばれた数字だ)。それより長い答え(多数のレコード、あるいは署名で軋むDNSSEC)はどうする?

そのときサーバーは**TC(Truncated)**フラグを立てる——「この答えは長すぎて切れた、TCPでもう一度聞き直せ」と。クライアントはそれを聞き、本格的なTCP接続を開いて一から尋ね直す。私はこの二段階のリズムを正確にコードに書かねばならず、そこで「TCPフォールバック付きUDP」が抽象的な言葉から、極めて具体的な数行のコードへと変わった。

そして**EDNS(0)が、あの古めかしい512バイト制限への礼儀正しいパッチとして登場する。クライアントが「私は4096バイトまで扱える」と告げる擬似レコードを付け、サーバーは大きなパケットをUDPでそのまま送り、TCPの回り道を省く。同じ一族に名前圧縮(name compression)**がある——一つのパケット内で、繰り返されるドメイン名を丸ごと書かず、すでに書いた位置を指し示す。数十年前、人々はこうして一バイトずつ絞り出さねばならなかった。その遺産は今も、あらゆるパケットの中で動いている。

プライバシーの衝撃:従来のDNSは丸裸だ

この部分には本当に手が止まった。

古典的なDNSは*平文(plaintext)*で送られる。暗号化されていない。これもまた1983年の素朴な信頼の遺産だ——インターネットが互いに知った数少ない研究機関だけだった頃、盗聴など誰も考えなかった。サイトを訪れるたびに「この名前のIPは?」という問いが、誰にでも聞こえるよう回線上で叫ばれる——あなたのISP、カフェのWi-Fiを運営する男、適切な場所に座る誰でも。

あなたはHTTPSで閲覧する。美しい緑の鍵、中身は固く封じられている。だがあなたが訪れる場所のリストは、DNSルックアップの段階から丸見えだ。手紙の中身を封じておきながら、宛先の住所を封筒に大きく書いて郵便局全体に読ませるようなものだ。

DoTDNS-over-TLS、専用ポートでTLSに包む)とDoHDNS-over-HTTPSDNSクエリを通常のHTTPSトラフィックに隠し、ウェブ閲覧とほぼ見分けがつかない)を実装したとき、近年ブラウザがなぜ静かにDoHをデフォルトで有効化してきたのかを理解した。見せかけではない——インターネットの土台に40年間眠っていた隙間を塞ぐためだ。

より広い教訓:あなたについて情報を漏らすのは、たいてい内容ではない——メタデータだ。 何を言うかではなく、誰に、いつ、何度言うか。

おもちゃと本物を分けるもの

DNSに正しく答えさせるのは半分にすぎない。難しい半分は、誰も優しくしてくれない現実世界に放り込まれても崩れないようにすることだ。

クライアントごとのレート制限DNSは増幅攻撃の好物だからだ——攻撃者は被害者の偽装IPで小さなパケットを送り、サーバーに大きなパケットをその被害者へ噴射させる。無制限のサーバーは、知らぬ間に大砲になる。ホットリロード:ゾーンを編集しSIGHUPシグナルを送れば、サーバーは即座に再読み込みする——再起動なし、処理中のリクエストを落とさない——本番で「切って入れ直す」は贅沢だからだ。そしてCPUコアごとに一つのソケット(SO_REUSEPORT)で、全パケットが一つの扉に押し合うのではなく、カーネルが負荷分散する。

どれもDNSを「賢く」はしない。ただ生き延びさせるだけだ。長く働くほど、私はこう信じる。自分のマシンで動くものと、現実世界で持ちこたえるものとの差は、ほぼ全て、デモでは誰も見せない部分に宿っている。

では、サイトが落ちたあの夜、私は何を学んだのか?

正直に言う。mini-dnsはBINDや、数十年戦い抜いてきた産業用DNSサーバーと張り合うために生まれたのではない。理解のための練習だ。そしてそれは、予想しなかった形で報いてくれた。

今、サイトが「開かない」とき、私はパニックで当てずっぽうをしない。占い師ではなく患者を診る医者のように、順番に問う。digは何を返す?TTLが切れていないから古いキャッシュがまだ生きているのか?Aレコードは正しいIPか?CNAMEがぐるぐる回っていないか?返っているのはNXDOMAINNODATAか?間違ったMXがメールを虚空に落としていないか?NSレコードは伝播し、互いに合意したか?

ブラックボックスは、私が理解するシステムになった。そしてそれは、この職業のほぼ全てに当てはまる。蓋を開けて中を覗く勇気を持つまで、あなたはその技術を本当には所有していない。 それまでは、他人の信頼を借り、自分の当番で壊れないことを祈っているだけだ。

考え続けている美しいことがある。ポール・モカペトリスが1983年、TOPS-20のマシン上で、数百台のコンピューターの痛みを解決するために素描したシステム——その設計が、今も惑星全体のために一日数兆のクエリを運び、その核はほとんど変わっていない。人々はセキュリティを、暗号化を、IPv6を継ぎ足したが、階層と委任の心臓はそのままだ。十分にシンプルで、十分に正しい良いアイデアは、帝国よりも長く生きられる。

あなたも「分解して作り直して学ぶ」タイプなら、mini-dnsをクローンして、ビルドしてみてほしい。

cargo build --release
./target/release/mini-dns

そして最初の問いを投げる。

dig @127.0.0.1 -p 8888 example.com A

数秒前に自分でゾーンに打ち込んだまさにそのIPが返ってくる瞬間——インターネットの広大で分散した電話帳が、少しだけ神秘を脱ぐ。それはもう魔法ではない。あなたが理解するものだ。

そしてエンジニアにとって、魔法を理解できるものに変えること——それはおそらく、最も静かで、最も長持ちする喜びだ。この奇妙な職業に私たちを留めておくもの、デプロイの夜を一つ、また一つと越えさせていくものなのだ。


全コード:github.com/uy-td-dev/mini-dns

関連する読み物

六つの駅、六つの死に方:あるHTTPリクエストの生存の旅システムデザイン
2026年6月14日3 min

六つの駅、六つの死に方:あるHTTPリクエストの生存の旅

技術ノートと省察 —— すべてのリクエストが辿らねばならない道について、そしてなぜその道を辿るのかについて。

読む
Nginxは気まぐれなんかじゃない — ただ設定の読み方を間違えているだけソフトウェア開発
2026年6月11日3 min

Nginxは気まぐれなんかじゃない — ただ設定の読み方を間違えているだけ

開発者の間には、こんな笑える矛盾があります。バックエンドをRustで書くべきか、Goか、それともNode.jsか — そんな議論なら一週間でも喜んで続けるのに、いざ本番にデプロイするとなると、90%の人は黙って apt install nginx と打ち込み、前段に置いてしまう。Nginxはみんなの定番「門番」であり、同時に、たった一つのシンプルなリクエストがなぜ404を返し続けるのか分からず、人を深夜2時までデバッグさせる張本人でもあります。 面白いのはここです。バグはほぼ確実にNginxにはありません。原因は、設定ファイルを上から下へ順番に実行されるスクリプトのように読んでしまうこと — Nginxはまったくそんな動き方をしないのに、です。 この記事は、サーバーにコピペするための「Nginxよくあるエラー集」ではありません。狙いは思考のモデルを手渡すこと。Nginxがどう「考える」のかを理解すれば、一見魔法のように見える罠が、画面を前に固まる代わりに、予測できるものに変わります。

読む
欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力ソフトウェア開発
2026年5月24日2 min

欲張りコード」を捨てれば開発は楽になる:実戦から学ぶ単一責任の原則(SRP)の力

本番環境で発生する重大なバグの多くは、アルゴリズムの不備ではなく、一箇所に責務を詰め込みすぎることから生まれます。本記事は、単一責任の原則(SRP)を実務的な視点から掘り下げます。「便利だから」という罠がどのようにGod Objectを生み出すのか、典型的な注文処理関数がなぜ保守の地雷と化すのか、そしてシニアエンジニアがそれをいかにしてクリーンでテスト可能な層へとリファクタリングするのか。クリーンな設計はタダではありません。しかし機能が100に膨れ上がったとき、それこそがシステムの崩壊を防ぐ唯一の支えとなるのです。

読む