中から外の住所で自分を呼ぶとき
一言でいうと
家の中から自分のグローバルドメインにアクセスすると、パケットがルーターまで行って折り返してこなければならないのに、多くのルーターはそれができません。これをヘアピンNAT(またはNATループバック)といいます。
なぜ必要なのか
家にサーバーを置いてlabhub.hopto.orgで公開すると、外からはうまくいくのに、家の中のノートPCからだけつながらないことがよくあります。原因は次のとおりです。
- ノートPC(192.168.0.10)が
labhub.hopto.orgを引きます。グローバルIP(1.2.3.4)が返ってきます - ノートPCが1.2.3.4へパケットを送ります。自分のサブネットの外なので、ルーターへ行きます
- ルーターは、1.2.3.4が自分自身だと気づき、ポートフォワーディングのルールに従って、宛先を192.168.0.20(サーバー)に変えます
- サーバーが応答します。ところが、応答の送信元は192.168.0.20で、ノートPCは1.2.3.4から来る応答を待っています
- ノートPCは、その応答を「自分が開いた覚えのない接続」と見なして捨てます
パケットがルーターでUターンするので、ヘアピン(髪留め)です。解決するには、ルーターが宛先だけでなく送信元も自分のアドレスに変える必要があります(SNATを一緒にかけます)。そうすれば、サーバーはルーターに応答し、ルーターがノートPCに返します。
どう動くのか
症状を確認する方法は単純です。
# 밖에서는 되는데 안에서만 안 되는가?
curl -sI https://labhub.hopto.org # 집 안에서 → 타임아웃
curl -sI http://192.168.0.20 # 사설 주소로 직접 → 됨
内部アドレスでは通るのに、ドメインでは通らなければ、ヘアピンです。
どう解くのか
ルーターがヘアピンをサポートしているなら、有効にすればよいです(設定名はメーカーごとに違います: NAT Loopback、Hairpin NAT、内部接続を許可)。
サポートしていなければ、スプリットホライズンDNSが定石です。同じドメインを、内側から尋ねればプライベートアドレスを、外側から尋ねればグローバルアドレスを答えるようにします。
- 内部DNS(例: ルーターのDNS、Pi-hole、CoreDNS)に
labhub.hopto.org → 192.168.0.20を入れます - そうすれば、家の中の機器は、そもそもルーターまで行きません
この方式がよい理由がもう1つあります。ヘアピンはトラフィックをルーターまで往復させるので、家の中の通信なのにルーターの性能に縛られます。スプリットDNSはスイッチの中で完結します。
よくある勘違い
hostsファイルで済ませること。ノートPC1台ではうまくいきますが、スマートフォンとタブレットはだめです。DNSの層で解決するのが正しい方法です。
グローバルIPが変わったら終わりだという勘違い。DDNSを使えばグローバルIPは追従しますが、内部DNSに書き込んだプライベートアドレスは、サーバーを移したら手で直す必要があります。2か所を管理する必要があることを覚えておきましょう。
内と外で違って見えるもの
ヘアピンは、「同じ名前が、場所によって別の経路になる」というより大きな問題の1つの事例です。同じ根から出る別の症状もあわせて知っておけば、見慣れない症状に出会ったときも、どこを見ればよいか見当がつきます。
証明書の名前が合わない。内部からプライベートアドレスで直接アクセスすると、証明書に書かれたドメインとアクセスしたアドレスが違うので、警告が出ます。そのため、スプリットDNSで同じ名前を使いながら、アドレスだけを変えることが重要です。アドレスでアクセスさせると、この問題がついてきます。
アクセス記録の送信元がすべて同じに見える。ルーターやプロキシが送信元を自分のアドレスに変えると、サーバー側のログには、すべてのリクエストが1つのアドレスから来たものとして残ります。そうなると、特定のユーザーを探せず、送信元を基準にした遮断やレート制限もすべて意味がなくなります。プロキシが元のアドレスをヘッダーに載せてくれる場合でも、そのヘッダーを信頼できるのは、自分たちが管理するプロキシが付けたときだけです。外から来た値をそのまま信じると、誰でも自分のアドレスを偽装できます。
同じサービスが内側でだけ遅い。トラフィックがルーターやゲートウェイまで出て戻ってくると、物理的には隣にある機器なのに、経路が長くなって、レイテンシが大きくなります。先ほど、スプリットDNSはスイッチの中で完結すると言ったのが、この部分です。
まとめると、原則は1つです。名前はどこでも同じにしておき、その名前が指すアドレスだけを場所によって変えます。名前を分けると(内部用のドメインを別に作ると)、証明書・設定・ドキュメントがすべて2セットになり、ある日片方だけが直されて、静かにずれ始めます。そして、2セットのうちどちらが正しいかは、たいてい障害が起きたあとで初めてわかります。名前を1つにするルールは、利便ではなく、事故を減らす仕組みです。内部でしか使わないサービスでも同じで、最初からきちんとした名前と証明書を付けておくほうが、あとで外に公開するときに、ずっと安く済みます。
実務で本当に大切なこと
Kubernetesにも同じ問題があります。Podが自分のServiceの外部アドレス(LoadBalancer IP)で自分自身を呼び出すと、同じヘアピンの状況になります。externalTrafficPolicy: Localを使うと、ノードがSNATを行わないので、この場合、応答が戻れないことがあります。
そのため、クラスターの中では、常にService名(svc.namespace.svc.cluster.local)で呼び出します。外部アドレスで自分を呼び出しません。この1行が、ヘアピン問題の半分をなくします。