NAT 背後の OpenVPN クライアントがインターネット出口になる仕組み
出口は自分から外へ接続するため、そのルーターに受信ポートは不要です。ハブが何をルーティングし、出口が何を転送し、戻り経路がどこから来るのかを追います。
三台の Linux ホスト、一つの設計:公開到達可能な OpenVPN ハブ、一般利用者のノート PC、そして内向きの転送を一切行わない普通の家庭用ルーターの背後にある出口マシン。出口は自分からハブへ接続する。消費者向けのインターネット通信はノート PC でトンネルに入り、出口自身の上流リンクから出ていく。この構成の驚くべき点はすべて 一つの詳細から導かれる:接続を始めたのが出口であるため、内側への経路はすでに存在し、それを作るためのポート転送は不要なのだ。
以下では設定の順序ではなくパケットの経路を追う。設定は、どの構成要素がどの判断を持つのかを知って初めて意味を持つからだ。各層はちょうど 一つの判断を持ち、誤りは 二つの層が 一つと見なされる場所に集まる。
二つのトランスポートセッション、一つの内部経路
各ホストはハブへの外向き接続をそれぞれ保持し、どちらも互いを知らない。消費者のインターネット向け通信は別個の内部フローであり、消費者のトンネル内で運ばれたあと、出口のトンネルから再送出される。ハブがルーティングすべきはこの内部フローであり、出口が転送すべきもこれである。
outer: consumer --TLS--> hub
outer: exit --TLS--> hub
inner: consumer --tun--> hub --internal--> exit --tun--> uplink
両方の外向き接続はクライアント側から出るため、出口のルーターは内向きの SYN を一度も見ず、宛先 NAT ルールも一度も必要としない。確立済みのトランスポートを維持するだけでよく、それは長寿命の接続に対してすでに行っている。公開インターネット上の何かが先に出口へ到達する必要はないので、その上流リンクが動的アドレスでも問題ない。ルーターは外向きセッションを許可する必要があるが、それはブラウザーがすでに使っている許可と同じだ。
iroute がサーバー内部に実際に置くもの
ハブだけが、どのピアがどの宛先を所有するかを決める装置であり、その判断をカーネルではなく自身のユーザー空間ルーティングテーブルで行う。出口に対する exit-node という名前の client-config-dir エントリは、そこでデフォルトプレフィックスを主張できる:
# /etc/openvpn/server/ccd/exit-node
iroute 0.0.0.0 0.0.0.0
この行はサーバープロセスに「任意の宛先を持つ内部パケットはこのクライアントのものだ」と伝える。ハブの OS にデフォルトルートを追加するわけでも、ハブをインターネットゲートウェイにするわけでもない。OpenVPN 2.6 リファレンス はクライアント固有の内部ルーティングとカーネルルーティングを別々の仕組みとして保っており、この分離こそがハブのデフォルトルートに触れずにこの設計が機能する理由である。
消費者には鏡像の指示が必要で、push として届けられ、自身の通信がローカルゲートウェイから出るのではなくトンネルに入るようにする。サブネットトポロジーではプールは出口の tun デバイス上の接続ルートなので、戻り方向に追加の push は要らない。出口が受け取ってはならないのは下のリダイレクトであり、それは使おうとしているトンネルへ自身の上流通信を送り返してしまう。送るのは消費者だけにし、CCD ファイルが選ばれたと信じるのではなくクライアント側で適用されたルートを読むこと。
# /etc/openvpn/server/ccd/consumer-01
push "redirect-gateway def1"
転送を行うのは出口のカーネル
出口の内部では、消費者からのパケットが tun インターフェースに到着し、とあるインターネットホスト宛てになっている。カーネルはルーティングテーブルを参照し、最良一致として実際の上流リンクを見つけ、次に 二つの独立した方針を適用する。このパケットをそもそも転送してよいか、その送信元アドレスでそのインターフェースから出してよいか、である。転送は sysctl であり、多くのディストリビューションでは既定で無効だ。
したがって出口には、常時有効な IPv4 転送、消費者のトンネル送信元を上流リンクへ通す転送ルール、tun を通る応答経路が必要である。ホスト全体で転送を有効にすると、そこを通る他のすべての挙動も変わるため、ルールはすべてを受け入れるのではなく送信元とインターフェースを名指しすべきだ。
戻り経路を買うのは送信元変換である
インターネットホストは 10.90.0.x アドレスへの経路を持たないため、応答は出口に届く前に破棄されてしまう。パケットが上流リンクを出るときに送信元を書き換えれば解決し、コネクション追跡が戻りでその書き換えを逆転させる。
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.90.0.0/24 -o enp1s0 -j MASQUERADE
ip -4 route get 10.90.0.9 # run on the exit: must point at the tun
上のルールは 一つの上流インターフェースの出発点であり、ファイアウォールポリシーではない。送信元は実際に許可した割り当てに絞り、出口自身の入力ポリシーは別に保つこと。そうしないとトンネル通信のマスカレードがローカルサービスを静かに露出させる。上流アドレスが動的なホストでは、マスカレードはアドレスを手で追う手間も省いてくれる。
ハブが強制できる場所とできない場所
client-to-client と従来のユーザー空間データパスでは、ハブはサーバープロセス内部でピア間に内部パケットを渡す。それらはハブのカーネル FORWARD チェーンを決して通らないため、そこのカーネルルールはどれほど丁寧に書いてもピア間の通過を制限できない。強制はサーバー自身の方針の中に置く必要がある。誰が接続できるか、どの CCD ファイルを受け取るか、出口のデフォルト所有を認めるかどうか、である。
だからこそ既定の iroute は広い信頼の判断である。出口は任意のインターネットアドレスからの戻り通信を正当に受け入れる必要があり、管理し、パッチを当て、監視するマシンでなければならない。同じ所有権を ルーターを任せない相手のピアに与える のが、この設計が招く誤りである。
本当の制御はメンバーシップである。自分のルートを設定できるクライアントは出口を使おうとしうるので、誰が接続できるか、出口がどのピアに提供するかを制限することは設計の一部であり、後付けの考えではない。
出口が消えたときに安全に失敗する
出口をオフラインにすれば、内部のデフォルト所有はそのセッションとともに消える。起きてはならないのは、ハブが静かに代替の出口になることだ。VPN プールからハブの上流リンクへのデフォルトルートを置かず、同じ通信を喜んで転送する広いマスカレードルールをハブに置かない。その失敗を意図的に試し、そのうえで回復はすでに開いていたページではなく新しいフローで試すこと。
# on the consumer, with the exit up and then down
ip -4 route get 1.1.1.1
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com
出口が消えたあとも消費者の通信がインターネットに届くなら、別の何かが運んでおり、意図した境界は存在しない。DNS と IPv6 も別々の判断として扱うこと。IPv4 のルートとマスカレードは AAAA 通信について何も語らず、ハブが押し込んだリゾルバーは問い合わせが実際に出ていく場所から答える。