コンテナは制御用ブリッジで応答し続けているのに、外向きのパケットはすべてオフィスの macvlan から出ています。何も壊れてはいません。二つの接続がそれぞれゲートウェイを提供し、唯一のデフォルトルートが意図しない側へ向いたため、サービスは動き続けたまま通信だけが遠回りしています。誰かが気づくころには、証拠はエラーメッセージではなくルーティングテーブルです。

よくある説明は、Docker が誤ってネットワークを選んだというものです。実際には、以前の判定は接続されたネットワーク名のアルファベット順に従っており、ルーティングがラベルの副作用になっていました。gw_priority はそれを、意図して選ぶ数値に置き換えます。レイアウトを書き留めてしまえば、導入はおよそ一分です。

デフォルトルートが誤ったネットワークに落ちる理由

サービスが参加する各ネットワークはそれぞれゲートウェイを提示できますが、コンテナが持つデフォルトルートは結局一本だけです。Compose 2.33.1 より前は、ネットワーク名を並べ替えて同着を決めていたため、control のように office より前に来るラベルが通信の行き先を決めていました。コンテナの内部には、その判断が恣意的だったことを記録するものが何もないため、コードレビューをすり抜けて後から奇妙な経路として現れます。compose ファイルにポリシーを書くことが、選択を検証可能にする唯一の方法です。

どのネットワークが勝つかを実際に変えるのは二つです。割り当てる gw_priority の値と、そもそもどのネットワークを接続するかです。三つ目のネットワークを足せば候補が増え、macvlan を外せばブリッジと競合する相手がなくなります。どちらの操作もアプリケーションログには現れません。誤ったデフォルトルートは接続を完全に切ることほとんどなく、応答が相手の想定しないアドレスから届くだけだからです。

gw_priority でゲートウェイを固定する

Compose はバージョン 2.33.1 以降、サービスとネットワークの接続ごとに gw_priority を公開し、最も大きい数値が勝ちます。サービス単位のゲートウェイキーは存在しないため、値はサービスの networks ブロックに置きます。以下の例ではデフォルトを control に渡し、office は直接接続された LAN のために到達可能なままにします。

services:
  vpn:
    networks:
      control:
        gw_priority: 1
      office:
        gw_priority: 0

networks:
  control:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.50.0/24
  office:
    driver: macvlan
    driver_opts:
      parent: eno1.120
      macvlan_mode: bridge
    ipam:
      config:
        - subnet: 192.168.120.0/24
          ip_range: 192.168.120.192/27
          gateway: 192.168.120.1

このブロックは二つの独立した制御として読んでください。gw_priority がデフォルトルートを決め、ipam の部分がアドレス指定を決めます。subnet だけを変えても出口の選択は変わらず、これがこの修正が中途半端に適用される最も一般的な形です。優先度はそれが統制するネットワークの隣に置き、次の読み手が判断を探し回らずに済むようにします。

gw_priority を隣にある単なる priority プロパティと混同しないでください。あちらは接続の作成順を決めるだけでルーティングには何も語らないため、デフォルトゲートウェイを選べず、インターフェース名も直しません。通信を誘導するために priority を上げるよう勧める人がいても、結果の順序がたまたま正しく見える場合でも、その助言は誤りです。

どのネットワークが実際にデフォルトルートを運ぶかを検証する

実行中のコンテナの内部で確認してください。ホストのルーティングテーブルはホストを表すもので、サービスが使う名前空間ではありません。まず構成チェックを実行して、打ち間違いが再起動前に失敗するようにし、そのうえでコンテナが実際に設定したルートを読み取ります。

docker compose config --quiet
docker exec vpn ip -4 route show default
docker exec vpn cat /proc/net/route

正しい結果ではブリッジのデバイスと 172.30.50.1 が示されます。誤っている場合は 192.168.120.1 と macvlan のデバイスが示されます。イメージに iproute2 パッケージが入っていなくても、/proc のファイルはデフォルトルートを報告し、宛先は 00000000、ゲートウェイのバイトは逆順になります。デバイス名が不明瞭なときは、インターフェースの列を docker inspect と突き合わせてください。

macvlan は物理ポートであり、ブリッジの複製ではない

macvlan はコンテナに有線上の第二の身元を与え、それこそがローカルデバイスへのアクセスを成立させます。同時に、スイッチポートとハイパーバイザが追加の MAC アドレスを受け入れる必要があり、多くのクラウドネットワークはそれを黙って拒否します。艦隊を計画する前に一つのコンテナで試してください。拒否された MAC は通常、リンクは上がっているのに何も通らない状態として現れます。

macvlan_mode: bridge という設定は macvlan ドライバのモードであり、Docker のソフトウェアブリッジではありません。passthru は既定値ではなく、別個の専門モードです。Linux はまた、macvlan コンテナが自身のホストに到達することも妨げます 同じ親インターフェースを経由するためであり、一つの接続に両方の役割を期待せず制御用ブリッジを残す理由がここにあります。

Compose が見る前に VLAN の親を名付ける

Docker は eno1.120 のようなドット付きの親を 802.1Q のサブインターフェースとして扱い、VLAN デバイスを自分で作成します。この便利さには限界があります。導出される名前は物理インターフェース名に依存し、長い名前は周囲のツールの想定を超える結果を押し出しかねません。ホストのネットワーク設定で名前付きの VLAN インターフェースを作り、parent をそこへ向ければ、両端が予測可能になります。

networks の下のキーは Compose の名前であり、Linux のデバイスではないことを忘れないでください。ホスト上で office という名前のインターフェースを探しても失敗します。カーネルが見るのは親インターフェースとコンテナ背後の veth ペアだけです。

その上に VPN の居場所を作る

VPN クライアントはこれらのネットワークが存在した後に接続するため、gw_priority とは無関係に、より具体的なルート、ポリシールール、フィルタを追加できます。遠隔のプレフィックスは両方のローカル subnet に対して計画し、制御経路が必要とする応答も含めて考えます。遠隔プレフィックスがオフィスの LAN と重なる場合は、トンネルからプライベート範囲を丸ごと引き抜くのではなく、アドレス計画を直してください。

VPN の起動後と再接続後の両方で、アドレス、ルート、サービスの経路を再確認してください。手で足したルートが消えるのはまさに再接続のときです。最初のテストの前に期待する表を書き留めておけば、後からの比較は記憶の確認ではなく差分になります。ネットワークの恒久的な所有権は Compose に置き、VPN の設定にはそれ自身のポリシーだけを持たせます。

関連記事