道と名前 — ルーティングテーブルと DNS の層
一言でいうと
ルーティングは「この宛先はどのインターフェースから、誰を経由するか」を最長プレフィックス一致で選ぶことで、Linuxはip_forwardを有効にしないと他のホスト宛てのパケットを転送しません。名前解決は/etc/hosts → スタブ → 上流サーバーという複数の層で行われ、digはそのうち最後の層しか見ません。
なぜ必要なのか
サブネットの外へ出るパケットは、ゲートウェイに任されます。そのゲートウェイは、また自分のルーティングテーブルを見て次のゲートウェイを選びます。この連鎖がインターネットです。各ホップが知っているのは「このアドレス帯はあちら」だけで、経路全体を知っている装置はありません。そのため、ルーティングの事故は1つのホップが1つのアドレス帯を知らないことから起きます。行きの道はあるのに帰りの道がない場合が特に多く、症状はただのtimeoutなので区別できません。
名前にも同じように層があります。昔は/etc/resolv.confにサーバーのアドレスがあり、それで終わりでした。今のUbuntuでは、systemd-resolvedが127.0.0.53でスタブとして待ち受け、リンクごとに異なる上流サーバーへ転送し、~domainルーティングルールで特定のドメインだけを特定のサーバーに送れます。VPNが社内ドメインだけを社内DNSに送るのが、この仕組みです。便利ですが、どの層が答えたのかがわからないと、「digでは引けるのにcurlでは動かない」を説明できません。
どう動くのか
ルーティングテーブルにdefault via 10.20.1.1と10.99.0.0/24 via 10.20.2.10があるとき、10.99.0.5は後ろの行へ向かいます。より長いプレフィックスが勝ち、書かれている順序は関係ありません。ip route get 10.99.0.5は、この判定をカーネルに直接尋ねて答えをもらいます。人がテーブルを読んで推論するより正確です。
Linuxは、デフォルトではルーターではありません。自分宛てでないパケットは捨てます。net.ipv4.ip_forward=1がそれを変え、値はネームスペースごとに別です。Dockerホスト・Kubernetesノード・VPNサーバーは、すべてこの値が1です。
名前解決は、アプリケーションがlibcのgetaddrinfoを呼ぶところから始まります。/etc/nsswitch.confのhosts: files dnsの順に、まず/etc/hostsを見て、なければresolv.confのスタブに尋ね、スタブはリンクの設定を見て上流へ転送します。digはこの層をすべて飛ばして、サーバーに直接尋ねます。そのため、サーバーが何を答えるかはdigで、アプリケーションが何を見ているかはgetent hostsで確認します。
現場での姿
帰りの道。新しいアドレス帯をつないで、ルーターに経路を入れました。pingが通りません。相手側のルーターが、新しいアドレス帯へ戻る経路を知らないのです。リクエストは到着しているのに、レスポンスが道に迷っています。両側でip route getを実行すると、片側だけがdefaultで見当違いの方向を指しています。
ポート53の衝突。dnsmasqをインストールしたのに起動しません。ss -ulpn 'sport = :53'を見ると、systemd-resolveが127.0.0.53を掴んでいます。解決策は、dnsmasqにlisten-addressとbind-interfacesを設定して、自分のアドレスだけを掴ませることです。
名前解決が遅い、または挙動がおかしいとき
ルーティングが合っているのに遅いとき、原因が名前解決側にあることがよくあります。層が複数あるので、どこで時間がかかっているのかが見えないからです。
検索ドメインが付く。resolv.confのsearchリストに複数のドメインが書かれていると、短い名前を尋ねるとき、そのリストを1つずつ付けて順番に試します。存在しない名前ほど試行回数が増え、サーバーの応答が遅ければその分だけ掛け算になります。KubernetesのPod内で外部ドメインの検索が遅くなる古典的な原因がこれで、そのため末尾にドットを付けて絶対名で尋ねることが解決策になります。
IPv6も一緒に尋ねる。getaddrinfoはAとAAAAの両方を尋ねますが、片方の問い合わせが失われると、タイムアウトまで待ってから再試行します。そのため「ときどき5秒ほど止まる」という症状が出ます。5秒はたいていリゾルバーのデフォルトのタイムアウト値なので、きっちり切りのいい遅延はタイムアウトを疑うというサインとして読めばよいでしょう。
キャッシュが古い。アドレスを変えたのに古い値が出続けるときは、どの層のキャッシュなのかを切り分ける必要があります。アプリケーション自体が結果を保持していることもあり(Javaランタイムが代表的に長くキャッシュします)、スタブリゾルバーが保持していることも、上流サーバーがTTLの間だけ保持していることもあります。TTLを先に短くしておいてからアドレスを変えるのが定石で、変える当日にTTLを短くしても手遅れです。
診断の順序は、先ほどの原則と同じです。まずgetent hostsでアプリケーションが実際に見ている答えを確認し、それがおかしければ/etc/hostsとスタブの設定を見て、サーバー自体を疑うときだけdigで直接尋ねます。digから始めると層を飛ばしてしまうため、アプリケーションが遭遇している問題を再現できないまま、「サーバーは正常」という結論だけを得ることになります。
次のラボですること
3つのネームスペースでホスト–ルーター–ホストを構築し、デフォルトルート・ip_forward・静的ルートを設定して、tracerouteでホップを数えます。そのあとdnsmasqで自分のDNSサーバーを立てて、resolvedのルーティングドメインにつなぎ、/etc/hostsがDNSに優先されることを、getentとdigで並べて確認します。