TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

ヘアピンNAT実測1: 社内からだけ自社ドメインにアクセスできない

ステータス: 実測(2026-11-01)。 家庭用ルーターの背後にあるTT Labクラスターで測定した値です。ルーター・クラスター・ネットワーク設定は一切変更せず、読込と一時ポッドのみを使用しました。アドレスとドメインはドキュメント用の例(192.0.2.0/24、203.0.113.0/24、 app.example.com)に置き換えており、時間と回数は実際の値です。 作成ルール: 実測データが基本であり、測定できなかったものは「未実測」、原因を確認できなかった解釈は「推定」と表示します。

目次

1. 結論から

ルーターの背後にWebサービスを公開し、ドメインを割り当てました。スマホのモバイルデータ通信なら問題なくアクセスできるのに、同じWi-Fiに接続したノートPCからはアクセスできません。サーバーも正常で、DNSも正しく設定されています。これがヘアピンNATの問題です。

Kubernetes内における同様の問題(ポッドが自身のServiceにリクエストを送信する場合)については、 パート2で解説します。

2. 何が問題なのか

自宅や小規模オフィスのルーターは通常、1つのグローバルアドレスを持ち、内部のデバイスにはプライベートアドレスを割り当てます。外部からのリクエストを内部のサーバーへ転送するためには、 ポートフォワーディングを設定します。「グローバルアドレスの443番に来たものはサーバー192.0.2.10の443番へ」といったルールです。ルーターは受信したパケットの 宛先アドレスをサーバーのアドレスに変更します。これをDNAT(Destination NAT)と呼びます。

外部からのアクセスに対してはこれで十分です。サーバーの応答はデフォルトルートに従ってルーターへ向かい、ルーターは記録しておいた接続情報に基づいて送信元をグローバルアドレスに戻してアクセス元へ送信します。アクセス元はグローバルアドレス宛てに送信し、グローバルアドレスから応答を受け取っているため問題ありません。

問題は、 アクセス元が同じプライベートネットワーク内にある場合です。ノートPC(192.0.2.20)が app.example.com を引くと、パブリックDNSはグローバルアドレス(203.0.113.7)を返します。ノートPCはそのアドレス宛てにリクエストを送信し、リクエストはデフォルトゲートウェイであるルーターへ向かいます。ここでルーターが行える動作は3つあります。

  1. 何もしない。 プライベートネットワーク側から入ってきたパケットにはポートフォワーディングを適用しません。ルーターはそのアドレスを自分宛てとして受信しますが、443番でリクエストを待ち受けていないため、破棄するか拒否します。
  2. 宛先のみを変更する(DNAT)。 下図のケースです。
  3. 宛先と送信元の両方を変更する(DNAT + SNAT)。 これがヘアピンNATです。

宛先だけを変更する場合

헤어핀 NAT 가 없을 때: 답이 다른 주소에서 온다 같은 사설망 안에서 서비스의 공인 주소로 접속하는 경우 클라이언트 192.0.2.20 공유기 안 192.0.2.1 밖 203.0.113.7 서버 192.0.2.10 192.0.2.20 → 203.0.113.7 ① 공인 주소로 보낸다 192.0.2.20 → 192.0.2.10 ② 목적지만 바꾼다(DNAT) 192.0.2.10 → 192.0.2.20 ③ 같은 망이라 공유기 없이 바로 답한다 클라이언트는 203.0.113.7 의 답을 기다린다 192.0.2.10 에서 온 답은 모르는 연결이라 버린다 연결이 맺어지지 않고 시간 초과로 끝난다
図: ヘアピンNATがない場合: ルーターが宛先のみを変更すると、サーバーは同じネットワーク内のクライアントへ直接応答を返し、クライアントはその応答を破棄する

ルーターが宛先のみをサーバーのアドレスに変更して送信した場合、サーバーが受信したパケットの送信元はノートPCのプライベートアドレス(192.0.2.20)となります。サーバーから見るとノートPCは 同一ネットワーク内の隣人であるため、応答をルーター経由ではなくノートPCへ直接送信します。応答パケットの送信元はサーバーのアドレス(192.0.2.10)になります。

ノートPCは203.0.113.7の443番と接続を確立しようとしてSYNを送信しました。しかし、返ってきたSYN-ACKの送信元は192.0.2.10です。ノートPCのTCPはこの応答を自分が確立しようとした接続に対するものとして一致させることができないため、未知のパケットとして処理します(通常はRSTで拒否します)。ノートPCは待ち望んでいた応答を最後まで受け取れず、SYNを数回再送したのちにタイムアウトとなります。ルーターも応答が自分を通過しなかったため、アドレスを元に戻す機会がありません。

要点は、 リクエストと応答で通る道が異なるということです。NATは、1つの接続における双方向の通信がどちらも自分を経由しなければ、アドレスを変換して元に戻すことができません。

送信元も変更する場合: ヘアピンNAT

헤어핀 NAT: 출발지도 바꿔 같은 길로 돌아오게 공유기가 목적지(DNAT)와 출발지(SNAT)를 함께 바꾼다 클라이언트 192.0.2.20 공유기 안 192.0.2.1 밖 203.0.113.7 서버 192.0.2.10 192.0.2.20 → 203.0.113.7 ① 공인 주소로 보낸다 192.0.2.1 → 192.0.2.10 ② 도착과 출발을 모두 바꾼다 192.0.2.10 → 192.0.2.1 ③ 답이 공유기로 돌아온다 203.0.113.7 → 192.0.2.20 ④ 되돌려 바꿔 전달한다 클라이언트는 기다리던 주소에서 답을 받는다 대가: 서버에는 사내 접속이 모두 공유기 주소로 보인다 규격(RFC 4787·5382)은 출발지를 공인 주소로 보이게 하라고 한다
図: ヘアピンNAT: ルーターが宛先と送信元の両方を変更することで、サーバーからの応答がルーターに戻り、ルーターがアドレスを元に戻してクライアントに転送する

ルーターが宛先とともに 送信元も自身のアドレスに 変更すると(②)、サーバーはリクエストがルーターから来たものと認識し、応答をルーターへ送ります(③)。ルーターは接続の記録に従って両方のアドレスを元に戻し、送信元を203.0.113.7としてノートPCへ渡します(④)。ノートPCは期待していたアドレスから応答を受け取ったため、接続が確立されます。パケットがルーターに入ってヘアピン(ヘアピンカーブ)のように折り返し、同じ方向へ出ていくため、ヘアピンNATと呼ばれます。ループバックNAT、NATリフレクションも同じ機能を指す名前です。

代償もあります。サーバー側からは、 プライベートネットワーク内からのすべてのアクセスが1台のルーターから来ているように 見えます。アクセスログから社内ユーザーを識別することができず、送信元アドレスに基づくアクセス制限やリクエスト数制限は、社内ユーザー全体を1人の人間としてカウントしてしまいます。また、社内トラフィックがルーターを2回通過するため、ルーターの処理負荷も増加します。

規格は何を要求しているか

NATの動作を定めたIETFのドキュメントでは、ヘアピンをオプション扱いしていません。

図では分かりやすくするために送信元をルーターの プライベート側のアドレス(192.0.2.1)として描いていますが、規格が要求する仕様は送信元が グローバルアドレスと外部で使用するポートに見えることです。どちらであっても応答がルーターに戻るという核心部分は同じです。Linuxでルーターを自作する際によく使われる「プライベートネットワークからの出発で、サーバー宛てであればMASQUERADE」というルールは、プライベート側のアドレスに変換する方式です。実際の家庭用ルーターの中には、規格に反してヘアピンをサポートしていなかったり、無効にできるようになっている製品があります(今回測定したルーターもセクション2の1または2に該当していました。どちらに該当するかはセクション5で確認します)。

3. 4つの解決策

ルーターを修正できればそれが最もクリーンですが、常にそれができるとは限りません。方法は大きく分けて4つあり、実際には2つ以上を組み合わせて使用します。

方法 動作内容 使用する場面 デメリット
スプリットDNS プライベートネットワーク内のDNSが同じ名前に対して 内部アドレスを返す 社内デバイスやクラスターが多く、DNSサーバーを自社運用しているとき 内と外で応答が異なるため、「DNSが正しいか」を確認する際に混乱する。内部アドレスが変更された場合、両方の場所を修正する必要がある。DoHのように社内DNSをバイパスするデバイスでは機能しない
hostsファイル 単一のデバイスでホスト名を内部アドレスに固定する 少数のデバイス、急ぎの場合 デバイスごとに手動で修正する必要があり、忘れやすい。ノートPCを持ち出して外出すると、外部からその名前でアクセスできなくなる
ルーターの設定 ルーターのヘアピン(NATループバック)機能を有効にする ルーターがサポートしており、設定を変更できるとき 製品によって名称や設定場所が異なり、機能自体が存在しない場合もある。サーバーからは社内からのアクセスがルーターのアドレスに見える
外部経由 社内デバイスが外部のプロキシ・トンネル・VPNを経由してグローバルアドレスにアクセスする 外部のユーザーが見ている経路をそのままテストする必要があるとき 遅く、コストがかかり、外部の機器に新たな障害ポイントが増える

いくつか補足します。

4. 測定方法

設定を変更せずに「同じ名前で、グローバルアドレスと内部アドレスそれぞれにアクセスする」というテストを3つの場所で比較しました。

場所 名前解決の方法
ノートPC(macOS、プライベートネットワーク) hostsファイルで名前を内部アドレスに固定している。グローバルアドレス側は --resolve で強制
クラスターノード(Ubuntu、プライベートネットワーク) hostsファイルに逆にグローバルアドレスが記載されている。内部アドレス側を --resolve で強制
クラスター内の一時ポッド クラスターDNS(CoreDNS)の hosts プラグインが名前に対して内部アドレスを返す(スプリットDNS)。グローバルアドレス側はTCP接続のみを試行

コマンドの形式は以下の通りです。試行ごとに5秒の制限時間を設けています。

# 공인 주소로 (DNS 를 건너뛰고 공인 주소를 강제, 이름은 그대로)
curl -sv --max-time 5 --resolve app.example.com:443:203.0.113.7 https://app.example.com/
# 내부 주소(게이트웨이)로
curl -sv --max-time 5 --resolve app.example.com:443:192.0.2.10 https://app.example.com/

外部からの比較としては、世界各地からHTTPリクエストを送信してくれる公開チェックサービス(check-host.netのHTTPチェック)を使用して4地点から行いました。

5. 実測結果

場所 グローバルアドレスへのアクセス 内部アドレスへのアクセス
ノートPC 3/3 タイムアウト(5.00〜5.01秒、接続段階で停止)。ポート80もタイムアウト 3/3 HTTP 200、接続2.8〜4.0ms、合計23〜28ms
クラスターノード 3/3 タイムアウト (5.00秒)。hosts のグローバルアドレスに従う通常の接続もタイムアウト 3/3 HTTP 200、全体 24〜31ms
一時 Pod TCP 接続タイムアウト (5.0秒) 名前が内部アドレスに解決され /healthz 200 (136ms、初回リクエストのため TLS 接続を含む)
外の4地点 3地点で HTTP 200 (0.78〜1.05秒)、1地点で「Connection reset by peer」 該当なし

外からは開き、内からはグローバルアドレスで開かないというヘアピン問題の症状がそのまま現れました。2026-10 の障害記録に「社内ヘアピンはブロック」と書き留めておきましたが、2026-11-01 でもそのままでした。外の1地点での接続拒否については、他の3地点が正常であるためその地点側の事情と見なしていますが、原因は確認できていません (推定)。

いくつかの項目を追加で測定しました。

6. TT Lab で発生した出来事

一般化してまとめると以下のようになります。

  1. 社内からのみサービスドメインにアクセスできませんでした。 外部からのアクセスには問題がなかったため、最初は社内端末の DNS やブラウザのキャッシュを疑いました。実際には、ルーターがヘアピン処理を行っていませんでした。
  2. 社内でのテストでは外部の状態を把握できませんでした。 外部からの接続が実際に切断される障害が発生した際、社内では普段から開かない名前であったため、「開かない」という結果が新しい情報になりませんでした。逆に、社内において内部アドレスで「接続できる」という結果も、外部からの接続を保証するものではありません。以降は確認を2つの経路に分けます。内部は --resolve でゲートウェイアドレスに直接アクセスし、外部は外部の監視サービスを使用して確認します。
  3. 環境ごとに異なる解決策を採用しました。 クラスター内には CoreDNS の hosts プラグインを使用してスプリット DNS を構成しました (Pod において名前が内部アドレスに解決されるのはこのためです)。開発用ノートPCについては hosts ファイルで固定しました。ルーターの設定には変更を加味していません。
  4. 忘れ去られていた hosts ファイルが存在していました。 あるノードの hosts ファイルには、名前がむしろ グローバルアドレスとして記述されていたため、そのノードからドメインへの接続は常に失敗していました。hosts ファイルは、「端末ごとに手動で設定し、そして忘れ去られる」という欠点がそのまま現れた事例です。

クラスター内から自身のグローバルアドレスへ向かう通信経路がブロックされている場合、cert-manager の HTTP-01 自己チェックのように「外部からはアクセスできるが、内部からの確認のみに失敗する」という現象が発生します。これについては、「 cert-manager はどのように証明書を作成し更新するのか」で解説しています。

7. 制限事項

8. まとめ: 「社内からだけ繋がらない」と言われたら

確認項目 理由
外部から最初に接続してみる (モバイルデータ通信、外部監視サービス) 外部からは接続でき、社内からは接続できない場合、ヘアピン問題である可能性が高い
社内から --resolve 이름:443:내부주소 で接続できる場合、サーバーは正常であり経路の問題である
社内からグローバルアドレスへの接続がタイムアウトするかどうか 拒否ではなくタイムアウトである場合、ルーターがパケットを破棄しているか宛先のみを変更しているケース
名前解決側での解決を図る スプリット DNS (複数台)、hosts (数台)、ルーターに機能があれば有効化する
忘れ去られた hosts ファイル 特定の端末でのみ挙動が異なる場合は、まず hosts ファイルを確認する
検証用端末の位置 クラスターノードで測定した「接続できる」という結果は、プライベートネットワークを経由していない可能性がある

TT の一言。 我が家の宛先で手紙を出したのに、郵便局が「同じ町内だね」と言って隣のおじさんの名前で返事を直接返してしまったら、私は見知らぬ人の手紙だと思って捨ててしまいます。郵便局が返事にもちゃんと郵便局のスタンプを押して送ってくれなければ、私は受け取ることができません。それができないなら、家の中では「隣の部屋!」と呼べばいいのです (スプリット DNS)!

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます