サービスはマシン上で動き、トンネルも上がっているのに、机の上のブラウザは依然として応答を待っている。よくある意味では何も壊れていない。アプリケーションはネットワーク名前空間の内部で待ち受けており、そのスタックはLANからは見えない。ループバックは名前空間に属するため、別のマシンのクライアントにはポートへ届くアドレスが存在しない。意図的なホップ一つ、あるいはvethペア一つで、ホストの残りを動かさずに直せる。

LANアドレスで何も応答しない理由

名前空間は完全なネットワークスタックである。インターフェース、ルート、ファイアウォール規則、そして専用のループバックを含む。ip netns exec 経由で起動したプロセスはそのスタックだけを見るため、バインドしたアドレスは名前空間の内部でしか解決されない。ホスト自身の 127.0.0.1 はまったく別のループバックである。

ホスト上で curl が失敗し、名前空間の内部では同じコマンドが成功するのはそのためだ。アプリケーションは正常であり、単にそのアドレスがホストのスタックに存在しない。

socatのホップで一つのポートを公開する

socat はユーザー空間で二つのスタックを橋渡しする。TCPポートを一つだけ通せばよい場合にはこれで足りる。ホスト側はLANで待ち受け、受け付けた接続ごとに名前空間の内部で二つ目の socat を起動し、アプリのループバックへ接続する。内部のプロセスはローカルクライアントとまったく同じようにアプリへ到達する。名前空間のルーティングは何も変わらないので、アプリはトンネルを唯一の出口として使い続ける。

socat TCP-LISTEN:9100,reuseaddr,fork EXEC:'ip netns exec vpn socat STDIO tcp-connect:127.0.0.1:9100',nofork

外側のプロセスには root か、名前空間へ入る権限が必要である。VPNが切れるとアプリは経路を失い、公開したポートは閉じたまま失敗する。牢獄に求めるべき挙動はこれだ。

複数のポートが通るならvethペアを使う

複数のポートやプロトコルを通す必要がある場合、あるいは他のホストが接続を開始する必要がある場合は、vethペアが正解だ。一方の端はホストに残し、もう一方は名前空間へ移し、小さな /30 が両側に安定したアドレスを与える。その後、ホストルートが名前空間のアドレスをペア越しに送る。

ip link add veth0 type veth peer name veth1
ip link set veth1 netns vpn
ip addr add 10.77.0.1/30 dev veth0
ip link set veth0 up
ip netns exec vpn ip addr add 10.77.0.2/30 dev veth1
ip netns exec vpn ip link set veth1 up

牢獄を壊すつもりがない限り、ホスト経由のデフォルトルートを追加してはいけない。トンネルは出口のままでなければならないので、ペア越しに到達できるのは公開したサービスアドレスだけだ。名前空間には専用のリゾルバファイルも与えること。ip netns exec は /etc/netns/vpn/resolv.conf を読むため、これがないと牢獄内の名前解決は空になる。

ホストではなく別のマシンから試す

テストは二台目のマシンから実行する。ホストはLANについて何も語らない経路でローカルのリスナーに到達できるからだ。まずアプリが名前空間の内部で実際に何にバインドしたかを確認し、そのうえで外から公開アドレスへ接続する。そこで全アドレスにバインドしても、ワイルドカードは依然として名前空間に属し、LANからは見えないため牢獄は広がらない。

ip netns exec vpn ss -lntp

ホストからの接続が成功してLANからの接続が失敗するなら、欠けているのはホップかルートであり、バインドアドレスではない。稼働中の tun デバイスを移すのではなく、名前空間の内部で OpenVPN を起動すること。インターフェースを移すと停止し、外から修復できないルートが消えることがある。復旧コマンドは牢獄の内部ではなく、コンソールに置いておく。

関連記事