ステータス: 実測(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の問題です。
- 原因はルーターです。 プライベートネットワーク内のクライアントがグローバルアドレス宛てに送信したリクエストを、ルーターが再びプライベートネットワーク内へ送り返す必要があり(ヘアピン)、その処理を行わないか、宛先のみを変更している場合は接続が確立されません。
- ヘアピンNATは、宛先(DNAT)とともに送信元(SNAT)も変更します。 これにより、サーバーからの応答がルーターを経由して戻るようになり、ルーターがアドレスを元に戻してクライアントに渡すことができます。NAT規格ではこれを「必ずサポートしなければならない(MUST)」と定めています。
- 修正すべき箇所を変更できない場合は、名前を変更します。 最も一般的な解決策は、プライベートネットワーク内でのみ同じドメインが内部アドレスを指すようにするスプリットDNSです。数台のデバイスであればhostsファイルでも対応可能です。
- 実測(設定変更なし): ノートPC・クラスターノード・ポッドの3箇所からグローバルアドレスへアクセスした場合、3回すべて5秒間のタイムアウトとなり、同じ名前で内部アドレスへ直接アクセスした場合は200となりました。外部の4地点のうち3地点からは200を受け取りました。
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つあります。
- 何もしない。 プライベートネットワーク側から入ってきたパケットにはポートフォワーディングを適用しません。ルーターはそのアドレスを自分宛てとして受信しますが、443番でリクエストを待ち受けていないため、破棄するか拒否します。
- 宛先のみを変更する(DNAT)。 下図のケースです。
- 宛先と送信元の両方を変更する(DNAT + SNAT)。 これがヘアピン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
ルーターが宛先とともに 送信元も自身のアドレスに 変更すると(②)、サーバーはリクエストがルーターから来たものと認識し、応答をルーターへ送ります(③)。ルーターは接続の記録に従って両方のアドレスを元に戻し、送信元を203.0.113.7としてノートPCへ渡します(④)。ノートPCは期待していたアドレスから応答を受け取ったため、接続が確立されます。パケットがルーターに入ってヘアピン(ヘアピンカーブ)のように折り返し、同じ方向へ出ていくため、ヘアピンNATと呼ばれます。ループバックNAT、NATリフレクションも同じ機能を指す名前です。
代償もあります。サーバー側からは、 プライベートネットワーク内からのすべてのアクセスが1台のルーターから来ているように 見えます。アクセスログから社内ユーザーを識別することができず、送信元アドレスに基づくアクセス制限やリクエスト数制限は、社内ユーザー全体を1人の人間としてカウントしてしまいます。また、社内トラフィックがルーターを2回通過するため、ルーターの処理負荷も増加します。
規格は何を要求しているか
NATの動作を定めたIETFのドキュメントでは、ヘアピンをオプション扱いしていません。
- RFC 4787 (UDP)REQ-9: NATはヘアピンを 必ず サポートしなければならず、ヘアピンによって送り返されたパケットの送信元は「外部の送信元IPアドレスとポート」(External source IP address and port)でなければなりません。
- RFC 5382 (TCP)セクション7.2およびREQ-8: TCPにおいても同様の要件となっています。
図では分かりやすくするために送信元をルーターの プライベート側のアドレス(192.0.2.1)として描いていますが、規格が要求する仕様は送信元が グローバルアドレスと外部で使用するポートに見えることです。どちらであっても応答がルーターに戻るという核心部分は同じです。Linuxでルーターを自作する際によく使われる「プライベートネットワークからの出発で、サーバー宛てであればMASQUERADE」というルールは、プライベート側のアドレスに変換する方式です。実際の家庭用ルーターの中には、規格に反してヘアピンをサポートしていなかったり、無効にできるようになっている製品があります(今回測定したルーターもセクション2の1または2に該当していました。どちらに該当するかはセクション5で確認します)。
3. 4つの解決策
ルーターを修正できればそれが最もクリーンですが、常にそれができるとは限りません。方法は大きく分けて4つあり、実際には2つ以上を組み合わせて使用します。
| 方法 | 動作内容 | 使用する場面 | デメリット |
|---|---|---|---|
| スプリットDNS | プライベートネットワーク内のDNSが同じ名前に対して 内部アドレスを返す | 社内デバイスやクラスターが多く、DNSサーバーを自社運用しているとき | 内と外で応答が異なるため、「DNSが正しいか」を確認する際に混乱する。内部アドレスが変更された場合、両方の場所を修正する必要がある。DoHのように社内DNSをバイパスするデバイスでは機能しない |
| hostsファイル | 単一のデバイスでホスト名を内部アドレスに固定する | 少数のデバイス、急ぎの場合 | デバイスごとに手動で修正する必要があり、忘れやすい。ノートPCを持ち出して外出すると、外部からその名前でアクセスできなくなる |
| ルーターの設定 | ルーターのヘアピン(NATループバック)機能を有効にする | ルーターがサポートしており、設定を変更できるとき | 製品によって名称や設定場所が異なり、機能自体が存在しない場合もある。サーバーからは社内からのアクセスがルーターのアドレスに見える |
| 外部経由 | 社内デバイスが外部のプロキシ・トンネル・VPNを経由してグローバルアドレスにアクセスする | 外部のユーザーが見ている経路をそのままテストする必要があるとき | 遅く、コストがかかり、外部の機器に新たな障害ポイントが増える |
いくつか補足します。
- スプリットDNSはTLSと相性が良いです。 ホスト名がそのまま維持されるため、証明書の名前やSNI・Hostヘッダーが変わりません。内部アドレスをIPで直接指定すると証明書のホスト名が一致しなくなりますが、スプリットDNSやhostsファイルであればこの問題は発生しません。特定のコマンドだけで一時的に変更したい場合は、
curl --resolve app.example.com:443:192.0.2.10がこのような処理を行います。 - スプリットDNSは「外部からのアクセス可能」を保証するものではありません。 社内で問題なくアクセスできるからといって、外部からもアクセスできるとは限りません。外部の状態は外部から確認する必要があります(セクション5)。
- ルーターの設定については今回確認していません。 設定を変更しないという原則のため、このルーターにヘアピンを有効にするメニューが存在するのか、また有効にした場合の動作については未実測です。
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地点が正常であるためその地点側の事情と見なしていますが、原因は確認できていません (推定)。
いくつかの項目を追加で測定しました。
- ルーターがグローバルアドレスを直接保持しています。 プライベートネットワークからグローバルアドレスに対して traceroute を実行したところ、最初のホップはそのグローバルアドレス自身であり、ping はノートPCから 0.65〜1.5ms、ノードから 0.37〜0.50ms で 3/3 応答しました。パケットはルーターまで届き、ルーターはそのアドレスを自身のものとして認識しています。通信事業者の二重 NAT (CGNAT) のような他のレイヤーが挟まっていないことを意味します。
- タイムアウトであり、拒否ではありませんでした。 グローバルアドレスの 443 ポートおよび 80 ポートで RST も ICMP エラーも受信しませんでした。第2節の3つのケースのうち、「何もしない (破棄する)」と「宛先のみを変更する」は、クライアントからはどちらもタイムアウトに見えます。
- どちらのケースであるかは特定できませんでした (未計測)。 宛先のみを変更するルーターであれば、ゲートウェイ側にノートPCのアドレスから送信された SYN が到達しているはずです。ゲートウェイノードのフロー記録 (Hubble) および接続追跡テーブルを確認しましたが、成功した内部接続においてもこの2箇所に L4 の記録が残らない構造 (ゲートウェイが L7 プロキシとして受信する構造) であるため、判別には使用できませんでした。サーバー側で
tcpdumpを確認できれば判別可能ですが、管理者権限が必要となるため今回は実施しませんでした。 - ノードで測定した「内部アドレス」はプライベートネットワークを経由していません。 ノードからゲートウェイアドレスへのリクエストは、そのノード自身の L7 プロキシによって処理された記録が残りました。Cilium がノードのホストネームスペースから LoadBalancer アドレスへ向かう接続をノード内で直接処理するためです。一方、ノートPCから送信されたリクエストは、ゲートウェイアドレスを担当するノードにノートPCのアドレス宛てとして到着した記録が残りました。「内部アドレスで接続できる」ことを確認する際は、 クラスター外の社内端末で測定して初めてプライベートネットワークの区間まで確認できます。
6. TT Lab で発生した出来事
一般化してまとめると以下のようになります。
- 社内からのみサービスドメインにアクセスできませんでした。 外部からのアクセスには問題がなかったため、最初は社内端末の DNS やブラウザのキャッシュを疑いました。実際には、ルーターがヘアピン処理を行っていませんでした。
- 社内でのテストでは外部の状態を把握できませんでした。 外部からの接続が実際に切断される障害が発生した際、社内では普段から開かない名前であったため、「開かない」という結果が新しい情報になりませんでした。逆に、社内において内部アドレスで「接続できる」という結果も、外部からの接続を保証するものではありません。以降は確認を2つの経路に分けます。内部は
--resolveでゲートウェイアドレスに直接アクセスし、外部は外部の監視サービスを使用して確認します。 - 環境ごとに異なる解決策を採用しました。 クラスター内には CoreDNS の
hostsプラグインを使用してスプリット DNS を構成しました (Pod において名前が内部アドレスに解決されるのはこのためです)。開発用ノートPCについては hosts ファイルで固定しました。ルーターの設定には変更を加味していません。 - 忘れ去られていた hosts ファイルが存在していました。 あるノードの hosts ファイルには、名前がむしろ グローバルアドレスとして記述されていたため、そのノードからドメインへの接続は常に失敗していました。hosts ファイルは、「端末ごとに手動で設定し、そして忘れ去られる」という欠点がそのまま現れた事例です。
クラスター内から自身のグローバルアドレスへ向かう通信経路がブロックされている場合、cert-manager の HTTP-01 自己チェックのように「外部からはアクセスできるが、内部からの確認のみに失敗する」という現象が発生します。これについては、「 cert-manager はどのように証明書を作成し更新するのか」で解説しています。
7. 制限事項
- 試行回数が少なくなっています。環境ごとに3回、外部は4地点で各1回ずつです。
- ルーターが1台のみです。ルーターが「パケットを破棄するのか」「宛先のみを変更するのか」を特定できず (第5節)、ヘアピンを有効化する設定の有無や、有効化した際の動作、有効化したときにサーバー側から見える送信元アドレスについては測定していません (未計測)。
- IPv4 のみで測定しました。サービス名に IPv6 アドレスが含まれていなかったため、IPv6 の場合は測定していません (未計測)。
- 外部の中継経路 (プロキシ、トンネル、VPN) は比較表に記載したのみで測定していません (未計測)。
8. まとめ: 「社内からだけ繋がらない」と言われたら
| 確認項目 | 理由 |
|---|---|
| 外部から最初に接続してみる (モバイルデータ通信、外部監視サービス) | 外部からは接続でき、社内からは接続できない場合、ヘアピン問題である可能性が高い |
社内から --resolve 이름:443:내부주소 |
で接続できる場合、サーバーは正常であり経路の問題である |
| 社内からグローバルアドレスへの接続がタイムアウトするかどうか | 拒否ではなくタイムアウトである場合、ルーターがパケットを破棄しているか宛先のみを変更しているケース |
| 名前解決側での解決を図る | スプリット DNS (複数台)、hosts (数台)、ルーターに機能があれば有効化する |
| 忘れ去られた hosts ファイル | 特定の端末でのみ挙動が異なる場合は、まず hosts ファイルを確認する |
| 検証用端末の位置 | クラスターノードで測定した「接続できる」という結果は、プライベートネットワークを経由していない可能性がある |
TT の一言。 我が家の宛先で手紙を出したのに、郵便局が「同じ町内だね」と言って隣のおじさんの名前で返事を直接返してしまったら、私は見知らぬ人の手紙だと思って捨ててしまいます。郵便局が返事にもちゃんと郵便局のスタンプを押して送ってくれなければ、私は受け取ることができません。それができないなら、家の中では「隣の部屋!」と呼べばいいのです (スプリット DNS)!