コンテナはオフィスの LAN では応答するのに、社内 VPN が接続した瞬間に応答をやめます。コンテナ側では何も変わっていません。再起動も、イメージ更新も、設定変更もしていないからです。変わったのはホストのルーティングテーブルで、トンネルが追加した経路がいま Docker の配ったアドレスを覆っています。サブネットを編集したり VPN の除外を依頼したりする前に、二つのアドレス計画を並べてください。修復にかかる時間は大きく違うからです。

設定を変える前に経路を読む

まず Docker が実際に作ったネットワークを見ます。IPAM ブロックがアドレス計画で、問題になるのはサブネットの行です:

docker network inspect project_default --format '{{json .IPAM.Config}}'
# [{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]

Compose は新しいプロジェクトごとに既定で /16 を割り当てるので、二つのサービスが 65,536 個のアドレスの範囲に収まります。この広さのせいで重複がよく起きます。無作為に選ばれた /16 は、トンネル経由で到達する拠点を丸ごと飲み込めるほど大きく、トンネルが上がるまで誰も気づきません。

次に、一つのコンテナアドレスについて経路選択を比べます。トンネルを下ろした状態と上げた状態で一回ずつです:

ip route get 172.18.0.7
# 172.18.0.7 dev br-3f2a9c src 172.18.0.1

答えの形は二通りあり、行き先が異なります。出力が tun0、wg0 など別のトンネルデバイスを指すなら、VPN のプレフィックスはブリッジ経路と少なくとも同じくらい具体的で、パケットはコンテナに届きません。それでもブリッジを指すなら、ホストはコンテナに到達できており、原因はフィルタリング、DNS、片方向だけ失敗する戻り経路のいずれかです。

なぜ /16 の衝突が起きやすいのか

Docker はブリッジネットワークをプライベートプールから取り、既定のプールは 172.16.0.0/12 ブロックを /16 ずつに切ったものです。企業ネットワークも同じプライベート空間をよく使ううえ、リモートアクセス VPN は接続した全クライアントにそのプレフィックスを押し出します。両側が 172.18.0.0/16 を自分のものだと思うと、一つのルーティングテーブルに矛盾した二つの主張が載ります。

勝者を決めるのはプレフィックスの長さで、経路が現れる順序ではありません。10.0.0.0/8 を押し出すトンネルは 172.18.0.0/16 のブリッジ経路には勝てません。/8 のほうが具体的でないからです。172.18.0.0/16、あるいは 172.18.5.0/24 の内側を押し出すトンネルなら勝てます。同じ VPN なのにオフィスでは動き、ホテルの Wi-Fi では失敗するのは、クライアントが受け取るプレフィックスが違うからです。

同じテーブルが逆方向も支配します。10.8.4.20 の内部ホストが必要なコンテナは、パケットをブリッジのゲートウェイに送り、ホストがそれをトンネルへ流します。この経路は通常は動きます。NAT を行うのがホストだからです。しかし、社内側がノート PC の VPN アドレスしか受け付けない場合や、逆経路フィルタが非対称な戻りを落とす場合は失敗します。

三つの修復を比べる

出口は三つしかなく、どれを誰が承認する必要があるか、適用中に何が壊れるかが違います。

RepairWho owns itWhat it costs
Renumber the container networkYouRecreates the network, restarts services, invalidates literal addresses in configs and scripts
Exclude your range from the tunnelNetwork teamChanges what every laptop sees, needs a change request and a client profile update
Reach the service through a published portYou and the clientOne port only, no direct container-to-container routing across the tunnel

チケットなしで終えられる唯一の修復が採番し直しで、多くの記事がそこで止まるのはそのためです。若いプロジェクトでは安く、移行ツールや保存済みの接続文字列がリテラルなアドレスを指した瞬間に高くつきます。YAML を編集する前にそれらの参照を数えてください。後では遅いです。

トンネルからの除外は、重複が会社が本当に所有する範囲にある場合に最もきれいな答えです。ただし最も遅く、クライアントプロファイルを変えられない請負業者には使えません。公開ポートや小さなリバースプロキシなら議論を避けられます。クライアントはトンネルがすでに正しく経路選択しているアドレスと話し、そのポートだけを到達可能にすればよいからです。

範囲は意図して固定する

一つのプロジェクトなら、compose ファイルでサブネットを宣言し、共有プールから取るのをやめさせます:

networks:
  default:
    ipam:
      config:
        - subnet: 10.201.7.0/24

その範囲はチームが管理する台帳から取り、検索で見つけた例から取らないでください。VPN が押し出すプレフィックス、人が作業する家庭内 LAN、同じホスト上の他のコンテナネットワークと突き合わせます。そのうえで作り直しを計画します。既存のネットワークは古いサブネットを保持するからです: docker compose down、次に up -d。docker network rm はエンドポイントが接続されている間は拒否し、ファイル内のどの ipv4_address エントリも新しい範囲に入っていなければなりません。

Docker 自身が作るネットワークには、/etc/docker/daemon.json でデーモンのプールを設定します:

{
  "default-address-pools": [
    { "base": "10.201.0.0/16", "size": 24 },
    { "base": "10.202.0.0/16", "size": 24 }
  ]
}

プールはデーモンの再起動後に作られるネットワークに適用され、既存のブリッジはアドレスを保ちます。便利な反面、忘れやすいので、誰かがネットワークを作り直すまで古いプロジェクトは重複したままとみなしてください。Docker の エンジンのネットワークに関するドキュメント には割り当て規則が書かれており、レビューでプールを説明するときに役立ちます。

Linux のブリッジと Docker Desktop は壊れ方が違う

Linux ホストでは、コンテナはホストの名前空間にあるブリッジ上に載ります。一つのテーブルがすべてを決めるので、上の比較がすべてです。ブリッジ経路を読み、トンネル経路を読み、実際のコンテナアドレスでどちらが勝つかを見てください。

Docker Desktop はコンテナを独自の NAT を持つ VM の背後に置きます。ホストはコンテナアドレスへの経路をまったく持ちません。公開ポート経由でサービスに届き、そのポートは通常 127.0.0.1 にバインドされます。したがって VPN クライアントは、サブネットの重複がなくてもサービスを壊せます。VM の内部範囲への経路を入れるか、トンネル接続中に loopback 通信を止めるかです。ホストで ip route get がコンテナアドレスへの経路なしと報告するなら、それは想定どおりで不具合ではありません。

実務上の結論は、最初にどこを見るかです。Linux ならルーティングテーブルから始めます。Desktop なら公開ポートと VPN クライアントのファイアウォール規則から始め、VM の内部サブネットがトンネルの押し出す何かと衝突していないか確認します。

トンネルを上げた状態で検証する

直ったと言う前に VPN を二回再接続してください。接続時に経路を押し出し、切断時に引き戻すプロファイルは、一回目の試行の後だけ正しく見えることがあります。そのうえで、クライアントが実際に実行する確認を流します:

  1. ip route get をホストから、コンテナの実際のアドレスを使って。
  2. 別のコンテナからの要求で、サービス自体が健全であることを確かめます。
  3. curl -sv http://127.0.0.1:8080/ をトンネルを上げた状態でクライアントから、次に traceroute -n を LAN 上のマシンからコンテナアドレスへ。
  4. sysctl net.ipv4.conf.all.rp_filter 要求がホストに届いても応答が消える場合に。

最後に、選んだ範囲を、選ぶのに使ったのと同じ台帳へ日付と所有プロジェクトとともに書き込みます。次の重複は誰も記録していないプロジェクトから来ます。範囲を無作為に引かず意図して予約しておくことだけが防御です。

関連記事