企業 VPN の先にある何にも届かないコンテナは、通常トンネルとは別のネットワーク名前空間に存在します。ホスト側クライアントのルート、DNS サーバー、ファイアウォールマークはホストのスタックに属し、ブリッジネットワークのコンテナは自前のスタックを持ちます。この一事実が、プラットフォームチームに届く報告のほとんどを説明し、「VPN が壊れている」と「Docker が VPN を迂回する」が同時に誤りになり得る理由でもあります。ひとつの症状の裏に二つのまったく異なる構成が隠れているため、設定を変える前にどちらなのかを特定してください。

トンネルがどの名前空間のものか突き止める

macOS や Windows の Docker Desktop では、コンテナは Linux 仮想マシンの中で動きます。デスクトップにインストールした VPN クライアントはデスクトップ OS にトンネルを作り、仮想マシンはそれを決して見ません。コンテナの設定では、必要なインターフェースがそのネットワーク名前空間にそもそも存在しないため直せません。

Linux ホストではトンネルはホストの名前空間にあり、ブリッジコンテナは通常、出方向で送信元 NAT を適用したホストのルーティングテーブルを通って到達します。その経路が失敗する原因は、欠けたルートではなく、転送ポリシー、逆経路フィルタリング、または重複するサブネットであることが多いです。読み進める前に、自分がどちらの世界にいるかを確認してください:

docker info --format '{{.OperatingSystem}}'
docker run --rm --net=host alpine ip route | head -5
docker run --rm alpine ip route | head -5

この二つのルーティングテーブルの違いが、問題のすべてを一画面で示します。ホスト側の表示にトンネルインターフェースがあり、ブリッジ側にないなら、それは名前空間の分離であり、残りはどう越えるかの選択です。

ホストネットワークは試験であり、設計ではない

コンテナをホストネットワークで動かすと、Docker がホストのネットワーク名前空間の共有として文書化しているとおり、トンネルインターフェースとそのルートを直接継承します。同じリクエストがそれで成功すれば、ルート理論が裏付けられ、失敗の原因は資格情報や DNS、アクセスルールではなく名前空間の分離です。

docker run --rm --net=host curl -sS -o /dev/null -w '%{http_code}\n' https://api.internal/health

これは診断として残してください。ホストネットワークはポート公開と、コンテナに求めた分離の大半を放棄し、また Docker Desktop では「host」は仮想マシンを指しデスクトップではないため、どちらのホストを相手にしていたかを忘れると、そこの成否に誤導されます。

tun デバイスとそれに伴う権限

コンテナ内の VPN クライアントにはトンネルデバイスと、それを設定する権限が必要です。よくある近道は --privileged で、すべてのデバイスを与え、コンテナ全体のセキュリティポリシーを緩めます。これはこの作業に必要な範囲をはるかに超えており、レビューで正当化するのは困難です。

sudo modprobe tun
docker run --rm -it --device /dev/net/tun --cap-add NET_ADMIN myvpn-client

より狭い形は単一のデバイスノードを割り当て、ネットワーク管理の権限だけを加えます。ノードが存在するよう、先にホストでモジュールを読み込んでください。コンテナが起動し、クライアントがトンネルデバイスを開けないと報告する場合は、そのエラーはたいてい文字どおりの意味なので、プロファイルを疑う前に割り当てを確認してください。

この設計は、クライアントがコンテナごとに動くことも意味します。各ワークロードが独自のトンネルを必要とするなら正解で、ホストのトンネルを見えるようにしたかっただけなら不正解で、プラットフォームチームは後者を想定していたのでしょう。

ブリッジ NAT は VPN が見る送信元を変える

既定のブリッジでは、コンテナの外向きトラフィックはホストのアドレスに変換されます。そのため VPN の先の企業側はコンテナではなくホストのトンネルアドレスを見て、送信元ごとのアクセスルールはホストが要求したかのように振る舞い、二つのコンテナは VPN サーバーにとって区別できなくなります。

macvlan や ipvlan は代わりに LAN アドレスをコンテナに与え、LAN 上の探索を助け、コンテナをセグメント上の対等な存在に見せます。ホストのトンネルも完全に迂回するため、その LAN から VPN に到達できなければ、VPN 専用のプレフィックスには届きません。

どちらの形も正当で、異なる要件に応えます。要件が「このコンテナは VPN 専用サービスに届くこと」なら、ホスト経由のルーティングとコンテナ内でのトンネル終端のどちらかを選びます。その選択は展開の前に、最初の失敗の後ではなく書き留めてください。どちらを選んでも、そのアクセスルールとログに現れるのは遠端が見る送信元アドレスだから、それを記録してください。そのアドレスを言えない設計は未完成です。

DNS はトンネルではなくデーモンに従う

コンテナは起動時に、デーモンかイメージからコピーされたリゾルバー設定を受け取ります。VPN が接続後に push するアドレスはそのコピーには遅すぎ、コンテナ内ではなくホストのリゾルバーに push されます。

サービス用にリゾルバーを明示的に設定するか、トンネル経由で到達できる内部リゾルバーを指してください。プライベート名をパブリックアドレスに解決するコンテナは通常パブリックリゾルバーを使っており、壊れたルートに悩んでいるのではなく、修正は compose ファイルかデーモン設定に属します。

同じ理屈は出口アドレスの問題にも当てはまります。各コンテナが異なる VPN アドレスで見える必要があるなら、ホストのトンネル一つでは実現できません、NAT がそれらを一つの送信元に潰すからです。実用的な形は、各コンテナの名前空間内のクライアントか、トンネルを所有しスタックを共有するサイドカーであり、どちらもアプリケーションのアドレス指定を変えます。

チケットを閉じる前のチェックリスト:トンネルを所有する名前空間を特定し、ホストネットワークで経路を証明し、必要な tun デバイスだけを割り当て、NAT 後の送信元アドレスを覚え、リゾルバーを明示的に設定し、コンテナごとの出口アドレスをフラグではなくアーキテクチャの決定として扱うこと。

関連記事