ネストしたWireGuard:中継ホップは自分のポートに応答できない
中継ホップがすべてを1つのトンネルに送ると、受信サービスへの応答が誤った経路で出ていく。インターフェースの送信元ポート規則がそれを直す。
三つのWireGuardホップが公開アドレスを持たないネットワークに到達すると、中継マシンは待ち受けるはずだったポートで応答しなくなる。先のトンネルは機能し続け、遠端のホストも応答し続け、外から静かになったのは中継サーバー自身だけだ。この組み合わせが紛らわしいのはまさに自作自演だからであり、中継ホップは自分のルーティングテーブルに従って、そのテーブルが応答を誤った方向へ送っている。
中継ボックスと話すまで機能するチェーン
例示的なチェーンを考えよう——クライアントAはUDP 500でサーバーBに到達し、BはUDP 500でサーバーCへ進む。Cは唯一のピアのアドレスからの通信だけを受け付けるので、葉になり、挙動が単純に保たれる。Bは葉ではなく、この障害の興味深い点はすべてそこにある。
この区別が重要なのは、葉と中継の役割が逆方向に引っ張るからだ。葉は何も他に依存されていないので、すべてを一つのトンネルに送っても支障がない。中継ホップはクライアントにとっての端点であると同時に遠端側へのクライアントでもあり、その二つの仕事は異なるルーティングを求める。
応答がトンネルから出ていく理由
Bでは、Cに向かうインターフェースが対応するデフォルトルートとともにAllowedIPs = 0.0.0.0/0を持つ。したがってBが生成するパケットは、より具体的なものが優先されない限り、そのトンネルに入る。
AからB宛てのパケットは、Bが宛先でありローカル配送が先に行われるため、到着して受け入れられる。応答はAの公開アドレス宛てにローカルで生成された真新しいパケットであり、その宛先はトンネルへのデフォルトルートに一致する。Cはこれを受け取るが、そのアドレスを所有していないので破棄する。
症状はファイアウォールの問題や鍵の不一致に見えるが、ここではどちらも正しくない。Cへ向かうトンネルでのキャプチャは、応答が誤った方向へ進んでいることを示す。この一つの観察は、どれだけ設定を読むよりも時間を節約するので、鍵を変え始める前に書き留めておこう。キャプチャを見ていない人は、トンネル自体が壊れていると考えるだろう。
狭い修復:送信元ポートに一致するポリシールール
Bの待ち受けソケットからの応答は送信元ポート500で出ていく。ポリシールールでこれに一致させ、そのパケットを通常のテーブルへ戻せる:
ip rule add sport 500 table main priority 100
ip rule add sport 8080 table main priority 101
ポリシールールは優先順に参照され、mainテーブルは32766にあるので、100のルールはトンネルのデフォルトルートより先に問い合わせられる。外から到達可能にしておく必要のあるポートごとに一件だけ追加し、それ以上は増やさない——そうすれば転送される通信は、そのために作られたトンネルを使い続ける。
この仕掛けの限界を知っておこう。sportセレクターはローカルで生成されたパケットに一致し、応答のケースを正確に覆うが、Bが他人のために転送するだけの通信は意図的に覆わない。Bの背後でホストされるサービスには依然として独自の応答が必要であり、そうでないふりをすると、最初のサービスでは機能し二つ目では失敗する修正になる。
ルールに所有者を与える
プロンプトで入力したルールは、次にインターフェースが再構築されると消える。WireGuardの設定ではインターフェースが作成・削除される場所に置く:
PostUp = ip rule add sport 500 table main priority 100
PostDown = ip rule del sport 500 table main priority 100
NetworkManager、ディストリビューションのフック、コンテナのエントリポイントがインターフェースを所有しているなら、そのペアは手編集のファイルではなく所有者の側に属する。最初の起動後だけでなく再接続後に有効なルールを確認したい——最初の起動は必ずうまくいくケースだからだ。
代替案を正直に比較する
送信元ポートのルールは四つの出口の一つであり、その中で最も狭い。トンネルのポリシーにまったく触れないという利点があり、そのため後の変更を生き延びやすい。狭い修正は適用されなくなると大きな音を立てて失敗するが、広い例外は理由が消えたずっと後まで静かに動き続ける。二年後に別の誰かがこの箱を所有するとき、その違いは優雅さよりも重要だ。
| Approach | What it costs | Where it breaks |
|---|---|---|
| Source-port policy rule | One rule per exposed port | Anything B forwards rather than originates |
| Destination exception route | A route pinned to specific client addresses | Clients whose address changes |
| Stop full-tunnelling on B | Loses the catch-all path toward C | Designs that depend on it |
| Split into two tunnels | More interfaces and more policy | Nothing, at the cost of complexity |
選んだら両端から検証する。AからBのポートへ接続を開始し、しばらくビジー状態に保つ。Bでは、ip route get <address-of-A> sport 500のようなものでカーネルに応答経路を正当化させ、トンネルではなくWANインターフェースを挙げていることを確認する。その後、Cの背後の何かがチェーン越しにまだ応答することを確認する——これは例外がバイパスに変わらず狭いままだったことを証明する。最後に、設計ノートにポートを記録しておかないと、次の運用者が別のサービスでこれを再発見する。