DockerコンテナのトラフィックだけをOpenVPN経由にする方法
ホストは通常のアップリンクのままにし、コンテナのサブネットだけをトンネルに入れます。NATの順序、送信元ルール、OpenVPNのiroute、MSSの確認が要点です。
コンテナのパケットがtun0に届くことはなく、コンテナに追加のケーパビリティを与えても変わりません。ゲストとトンネルの間には経路検索が二回あり、一回はゲストのネットワーク名前空間の内側、もう一回はフレームがブリッジに着地した後のホスト上で、二回目は照合規則の異なる別のテーブルを参照します。私の参照構成は、OpenVPNクライアントで、10.8.0.10をtun0上に持ち、172.18.0.0/16のDockerブリッジと、トンネル側からしか到達できないプライベートアドレスを備えています。目標は狭いままで、ゲストのサブネットだけがトンネルから出て、ホストは通常のアップリンクを保ちます。
コンテナ内でVPNクライアントを動かすのは別の設計であり、近道ではありません。コンテナが自身のトンネルと資格情報を持ち、作業はiptablesに残らずDockerfileへ移ります。
転送は宛先で経路を選び、その後NATが送信元を書き換える
まず net.ipv4.ip_forward=1を設定すると、転送パケットはメインテーブルで宛先だけで経路選択されます。送信元はその判断に関与しません。経路が選ばれた後にだけカーネルはPOSTROUTINGを実行し、そこがDocker自身のルールの置き場所です:-s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE。ブリッジを離れて別のインターフェースへ向かうものはすべて変換され、それがどのアドレスになるかは、経路が送り出したインターフェース次第です。
このルールが、私がよく聞かれる症状のほとんどを説明します。メインテーブルにプライベート宛先の経路がなければ、パケットはホストの物理アドレスを載せてアップリンクから出ます。プロバイダが宛先を落とすか、遠隔サイトが想定外のRFC 1918送信元を落とし、トンネルは上がったまま健全なのにコンテナがタイムアウトします。tun0でのキャプチャには何も映らず、それが手がかりです。経路が本当にtun0を指しているなら、MASQUERADEが送信元を10.8.0.10に書き換え、同じコンテナが何も触らずに動きます。
ゲストの送信元を示す検索を依頼してください。fromとiifがなければ、ホストは自分のソケットについて答え、転送パケットが決して通らない経路を報告します:
ip route get 192.168.11.222 from 172.18.0.5 iif br-appnetその答えがアップリンクを指すなら、トンネルは関与しておらず、トンネル側の設定では何も解決しません。
ゲストをトンネル自身のアドレスへ移す
最小の有効な変更は、出口での明示的な送信元の書き換えです。追加するのではなく挿入します:DockerはすでにPOSTROUTINGの先頭に座っているため、-Aで追加したルールはDockerのルールより後で評価され、決して発火しません。
iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10これが保つかどうかは二つの条件で決まります。アドレスはクライアントが実際に保持しているものでなければならないので、クライアントごとのエントリにifconfig-push 10.8.0.10 255.255.255.0を含めて固定します。決して割り当てられなかったアドレスを名指すルールはエラーではなく静かなドロップを生み、遠隔側がその送信元を受け入れ、応答を戻す経路を持たなければならないからこそ、向こうの許可リストにはゲストのサブネットではなくトンネルのアドレスを書きます。クライアントのアドレスが動的な場合は、-j MASQUERADEと-o tun0でインターフェースが持つ値に従いますが、固定値に対して書けない遠隔ポリシーという代償があります。
フィルタリングがもう半分です。DockerはFORWARDポリシーをdropにし、デーモン再起動のたびに自分のチェーンをフラッシュするので、ローカルポリシーはDOCKER-USERに入れます(これは生き残ります):
iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT戻りのルールが重要です:tun0に届く応答は、ブリッジ経由で開いた接続に対するものであっても、外向きが許可されたという理由だけでは許可されません。
送信元ルールでホストを直接のまま保つ
ブリッジ全体はトンネルを使うべきだが、ホスト上の他の何も使うべきでないなら、NATを飛ばしてその送信元に専用のテーブルを与えます。ゲストのアドレスはそのまま端から端まで見えます。
echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnboxルールはメインテーブルより先に参照され、fromが送信元に一致します。トンネル自身の外側パケットはホストの物理送信元を運ぶので、from 172.18.0.0/16には決して一致せず、トンネルが自分自身を自身へ戻す経路を持つことはありません。この自己除外が構成を安定させます。
戻りの経路には今、助けが必要です。遠隔側は変換されていない172.18.0.xを見て、それがどこにあるかを知る理由がありません。サーバ側ではクライアントごとのファイルにiroute 172.18.0.0 255.255.0.0が必要で、サーバの内部テーブルがそのプレフィックスをこのクライアントに渡し、さらにプッシュするroute 172.18.0.0 255.255.0.0が必要で、他のクライアントはそれが存在すると分かります。
信頼する前に二つの確認をします。実行するのはip route show defaultです:redirect-gateway def1ではテーブルに0.0.0.0/1と128.0.0.0/1がtun0経由で入り、ホストはまったく直接ではありません—なぜならそれら半分のデフォルトはdefaultを置き換えずに勝つからです。ip rule addで追加したルールも再起動で消えるので、upスクリプトに入れてください。
パケットサイズは二番目に出会う失敗です
1500バイトのブリッジインターフェースは、外側のUDPとIPヘッダーが加わると、トンネルが運べる量を超えて渡します。小さなやり取りは通り、DNSは解決しTLSハンドシェイクも完了しますが、その後の大きなアップロードはどこにもエラーを出さずに止まります。送信側は同じセグメントサイズを再試行し続けます—縮めるべきだと伝えるICMPのフラグメンテーション要求が決して届かないからです。
インターフェースで、MSSで、あるいは両方で直します。クランプにゲストの協力は要りません:
ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuクランプはTCPしか書き換えません。UDP、QUICを含むものは、代わりにゲストのインターフェースを下げる必要があり、--opt com.docker.network.driver.mtu=1400をネットワーク作成時か、対応するcomposeキーで指定します。
DNSと繰り返す価値のある確認
名前解決も同じ経路をたどります。Dockerの組み込みリゾルバは127.0.0.11でホストの設定済みサーバーへ転送するので、コンテナはホストが使うものを継承します。プライベートゾーンがトンネル経由でしか届かないリゾルバだけに答える場合、ゲストをそのアドレスに向け、getent hostsでコンテナ内から試してください(ホスト上ではありません)。
- 失敗するリクエストはゲスト内で実行してください—ホスト側の試験は別の送信元アドレスを使い、問題を隠します。
- 追加したNATとフィルタのルールのカウンタを読んでください。一致がゼロのルールは経路の誤りであり、ポリシーの誤りではありません。
- SNATルールのトンネルアドレスが、クライアントが実際に保持しているものか確認してください。
- 停止をサーバーのせいにする前に、ゲストのMTUを下げるかMSSをクランプしてください。
そのリストを下っていき、最初の失敗で止まります。直すべきはその層で、それより下に何を足しても、壊れている境界を隠すだけです。