TT Lab
はじめる
学ぶ 学習パス コース

アドレス・サブネット・ゲートウェイ

ヘアピンを再現し、二つの方法で解く

TT Labで続きを見る

目標

家の中のノートPC・ルーター・サーバーの3つの役割を、1つのPodの中に作り、宛先だけを変えたときに応答が戻れないことを実際に再現します。次に、送信元まで変えて通るようにし、同じ問題を名前解決の側で解く方法と比べます。

なぜ重要なのか

「外からは通るのに、家の中でだけ通らない」という報告は、原因を推測するのが難しいものです。接続できないのでも、名前が解決できないのでもなく、応答は来るのに、受け取る側が捨ててしまうからです。捨てられることは、ログに何も残しません。

このラボは、その捨てられる瞬間を目で見えるようにします。connect()したUDPソケットは、接続した相手から来たものだけを受け取り、残りはカーネルが黙って捨てますが、それは、ノートPCがルーターのアドレスから来る応答を待っていて、サーバーのプライベートアドレスから来た応答を捨てる状況と、まったく同じです。

このPodでできることとできないこと

ラボのPodにはカーネル権限がないので、ip netnsもnftもtcpdumpも動作しません。その代わり、127.0.0.0/8は範囲全体が自分自身なので、権限なしでも、127.0.0.10・127.0.0.20・127.0.0.99を1台で一緒に使えます。3つの役割を、その3つのアドレスに分けて使います。ステップ6のnftablesのルールは、文章として書くだけで、適用はしません。

ステップ

  1. 3つの役割のアドレスとやることを、/root/hairpin/01-map.txtファイルに書いてください。laptop 127.0.0.10、router 127.0.0.99、server 127.0.0.20で、3つのアドレスを1台で使える理由も1行書きます。
  2. /root/hairpin/site/index.htmlファイルにhairpin-lab-serverの1行を入れ、そのディレクトリを127.0.0.20:9000で公開してください。プライベートアドレスでつないだ結果と、127.0.0.99:9000でつないだ結果を、/root/hairpin/02-direct.txtファイルに一緒に書き込みます。
  3. 宛先だけを変えたとき(DNATだけ)に、パケットの送信元と宛先がどう変わるかを、/root/hairpin/03-tuple.txtファイルに5行で書いてください。行頭は、それぞれrequest-out、after-dnat、reply、expected、mismatch=です。
  4. /root/hairpin/nat.pyファイルを作成してください。python3 nat.py dnat-onlyが、3つのソケットでその状況を再現し、最後の行にresult=droppedを出力する必要があります。実行した結果を、/root/hairpin/04-dnat-only.txtファイルに保存してください。
  5. 同じツールのpython3 nat.py dnat-snatが、送信元まで変えてresult=deliveredを出すようにして、結果を、/root/hairpin/05-dnat-snat.txtファイルに保存してください。
  6. 本物のルーターにかけるルールを、/root/hairpin/06-rules.nftファイルに書いてください。サーバーは192.168.0.20、家の中の範囲は192.168.0.0/24、ポートは80です。preroutingとpostroutingの2つのチェーンが必要で、送信元を変えるルールは家の中から来たものにだけかかる必要があります。
  7. 同じ問題を、名前の側で解いてください。/root/hairpin/hosts.internalファイルにhome.example.testの内部での答えを書き、curl --resolveで内部の答えと外側の答えをそれぞれ真似て、/root/hairpin/07-split.txtファイルに書き込みます。
  8. 2つの解法を、/root/hairpin/08-compare.mdファイルで比較してください。## 헤어핀、## 스플릿、## 무엇을 고르나(それぞれ韓国語で「ヘアピン」「スプリット」「何を選ぶか」を意味する見出しです)の3つの節が必要です。

参考

3つの役割の地図を描く

127.0.0.0/8は、第1オクテットが127ならすべて自分自身です。そのため、127.0.0.20にサーバーを結びつけてもよく、127.0.0.99をルーターの役割に使ってもかまいません。カーネル権限が必要ないことが、このラボがPodで動く理由です。

1行に名前・アドレス・やることを書き、最後の行に、3つのアドレスを1台で一緒に使える理由を書いてください。

プライベートアドレスでは通るのに、外側のアドレスでは通らない

まず、サーバーが公開するファイルを作ります。採点ツールがレスポンスを見分けられるように、hairpin-lab-serverという文字が入っている必要があります。

python3 -m http.server 9000 --bind 127.0.0.20 --directory /root/hairpin/site

そのあと2回つないでみます。127.0.0.20:9000は通り、127.0.0.99:9000は通りません。今はルーターの役割に何もないので、すぐに拒否されますが、これが「ポートフォワーディングをかけていないルーター」の役割です。

失敗した側のメッセージもファイルに書き込む必要があります。curl -sS -m 3 http://127.0.0.99:9000/ 2>&1のように、標準エラーも一緒に受け取ってください。

4つのタプルを追跡する

DNATは宛先だけを変えます。そのため、ルーターを通ったあとも、送信元はノートPCのままで、サーバーはノートPCに直接答えます。

4行を順に書いてください。

最後のmismatch=の行には、replyとexpectedがどこでずれているかを書きます。ポート番号は、適当な値を選んで一貫して使えば十分です。

宛先だけを変えると、応答が捨てられる

核心はconnect()です。UDPソケットでも、connect()すると、カーネルに「誰から来る応答を待っているか」が登録され、別のアドレスから来たデータグラムは、ソケットに届かないまま捨てられます。

ソケット3つを次のように置きます。

laptop  bind(('127.0.0.10', 0))  connect(('127.0.0.99', 9500))
router  bind(('127.0.0.99', 9500))
server  bind(('127.0.0.20', 9500))

ノートPCが送ると、ルーターが受け取ります。DNATだけがかかった状況なので、サーバーが見る送信元はノートPCでなければなりませんが、ソケットでは送信元を偽装できません。そのため、元の送信元を本文に載せて送り、サーバーがそのアドレスに直接答えるようにしてください。

最後にlaptop.recv()を2秒の制限で試し、socket.timeoutが出たら、result=droppedを出力します。

送信元まで変えると通る

直すのは1つだけです。ルーターがリクエストをサーバーへ渡すとき、自分のソケットで送れば、サーバーが見る送信元がルーターになり、応答もルーターに来ます。ルーターは、その応答をノートPCに返せばよいのです。

このとき、ノートPCが受け取る応答の送信元は127.0.0.99:9500で、それがノートPCがconnect()した相手と同じなので、カーネルが通します。

同じツールをモードだけ変えて実行するということが重要です。採点ツールが2つのモードを両方実行して、結果が実際に分かれるかを見ます。

ルーターにかけるルールを書く

このPodにはカーネル権限がないので、nftで適用できません。ルールを正確に書くことが目的です。

チェーンが2つ必要です。

chain prerouting { type nat hook prerouting priority dstnat; … dnat to … }
chain postrouting { type nat hook postrouting priority srcnat; … masquerade }

送信元を変えるルールに、条件を書き漏らさないでください。条件なしでかけると、外から来たリクエストまで送信元がルーターに変わり、サーバーログのアクセス記録がすべて1つのアドレスにまとまってしまいます。ip saddrで、家の中の範囲から来たものだけを選んでください。

ルールごとに、何をする行かを#のコメントで付けます。

同じ問題を名前の側で解く

スプリットホライズンDNSは、同じ名前を、内側から尋ねればプライベートアドレスを、外側から尋ねればグローバルアドレスを答えるようにするものです。そうすれば、家の中の機器は、そもそもルーターまで行きません。

DNSサーバーを立てずに、curl --resolveで2つのケースを真似ます。

curl -sS -m 3 --resolve home.example.test:9000:127.0.0.20 http://home.example.test:9000/
curl -sS -m 3 --resolve home.example.test:9000:127.0.0.99 http://home.example.test:9000/ 2>&1

アドレスではなく名前でアクセスするということが核心です。プライベートアドレスで直接つなぐようにすると、証明書に書かれた名前とアクセスしたアドレスが違うので、警告が出ます。

hosts.internalには、内部で答える値だけを置きます。外側のアドレスも一緒に書くと、それはもうスプリットではありません。

2つの解法を比較する

2つの解法は、直す場所が違います。ヘアピンはルーター1台だけを触ればよいのですが、トラフィックがルーターを往復します。スプリットDNSはスイッチの中で完結しますが、管理する場所が2つになります。

比較に必ず入れるものは3つです。

節の見出し## 헤어핀、## 스플릿、## 무엇을 고르나は、採点ツールがそのまま探します(それぞれ韓国語で「ヘアピン」「スプリット」「何を選ぶか」を意味する見出しです)。