一台のLinuxゲートウェイで二つのOpenVPNクライアント:ポリシーを分ける
両方のトンネルが起動していても、クライアントは同じ一方を使う。送信元で決め、各グループに専用のテーブルを与え、選択したデバイスで許可と変換を行う。
二つのOpenVPNデーモンがどちらもトンネル成功を報告しても、ゲートウェイはすべてのクライアントを同じ一方へ送っている。どのトンネルを使うかをハンドシェイクで決めるものは何もなく、決めるのはカーネルであり、パケットごとに、ポリシーのルールをたどり、次に各ルールが指すテーブルをたどる。そう見れば、設定はOpenVPNのオプションの話ではなく、二人の所有者を持つルーティング行列になる。
決めるのはカーネルであり、デーモンではない
転送されるパケットはLANインターフェースに到着し、ルーティングコードがルール一覧を優先順に評価する。セレクタが一致する最初のルールがテーブルを指し、そのテーブルのルートが出力デバイスを選ぶ。テーブルに一致するルートがなければ検索は次のルールへ続き、この細部のおかげで中途半端な設定は大きな音ではなく静かに失敗する。
そのたどり方にトンネルプロセスの口出しはない。インターフェースを作り、ルートを入れることはできるが、特定のクライアントのパケットがそのインターフェースに入るかどうかは、どのテーブルへ送られたかで完全に決まる。だから同じ取り込みルートを持つ二つのトンネルは競合し、最後に書いた者が全員分を勝ち取る。設定ファイル上は釣り合って見えても、すべてのクライアントを一つのデーモン経由で送ってしまうのはそのためだ。
各インスタンスに専用のリソース一式を与える
名前から始める、使い回した名前こそ、二つのインスタンスが偶然に状態を共有してしまう道だからだ。トンネルデバイス、設定ファイル、資格情報、サービスユニットをそれぞれ別にする。各インスタンスの外側エンドポイントとトンネルアドレスを、ポリシー行列と同じテーブルに記録しておけば、後の変更は一か所を見ればよい。
dev tun-office
route-nopull
pull-filter ignore "redirect-gateway"
dev tun-lab
route-nopull
route-nopull はサーバーが押し出すルートをローカルテーブルの外に保つ。ポリシーのテーブルを明示的にするなら、まさにそれが欲しい動きであり、あるサーバーが redirect-gateway オプションも押し出すか、クライアントの版がフィルタ形式に対応するかは、推測せず手引きで確かめる価値がある。
行列を書く:送信元、テーブル、デバイス
クライアントのグループごとに、送信元アドレスでテーブルを選ぶルールを追加し、そのテーブルに意図した経路を入れる:
ip rule add from 10.8.1.0/24 lookup 100 priority 1000
ip rule add from 10.9.1.0/24 lookup 200 priority 1000
ip route add default dev tun-office table 100
ip route add default dev tun-lab table 200
テーブルには、そのグループが持つべき宛先ルートと、ゲートウェイを使えるままに保つローカルや管理用の例外が必要だ。既定ルートだけを持つテーブルは、そのグループに対する全トンネルのポリシーであり、それが正しいこともあるが、抜けではなく判断であるべきだ。
トンネル自身のトラフィックはクライアントグループにない
各デーモンの暗号化パケットはゲートウェイ自身から生まれ、外側のピア宛てだ。クライアントの送信元プレフィックスだけに一致するルールはそれらのパケットをそのままにする、そのため送信元で絞ったルールは、すべてに一致するルールより安全なのだ。セレクタのないルールはトンネル自身のトラフィックを捕まえ、それをまたトンネルへ送り返すことができ、そのとき接続はプロバイダーの問題に見える形で失敗する。
ローカルの管理セッションと下位層の経路は、ポリシーのテーブルの外に明示的に置く。その例外は行列のそばに書き留める——設計の一部であり、障害時に再発見する回避策ではない。
経路を選ぶことは許可ではない
転送の許可は、経路の後半で下される二つ目の判断だ。二つ目のトンネルから出るパケットを受け入れるファイアウォールルールは、ルーティングコードにそれを選ばせないし、正しいテーブルは転送の許可を与えない。両方が必要であり、片方だけを証明する試験はもう片方について誤解させる。片方のグループが動き、もう片方が動かないときは、テーブルを書き直すのではなく、この二つの段階を別々に確認する。
送信元の変換は三つ目の判断であり、送信元グループと、実際にそれを運ぶトンネルデバイスに限定すべきだ。postrouting チェーンを空にするのではなく、二つのインスタンス専用のチェーンを追加する——無関係なサービスがそこに項目を持っていることがあり、空にするとそれらが静かに消えるからだ。
トンネルが一本落ちたときに何が起きるか
障害時の振る舞いは前もって決める。トンネルのテーブルがルートを失うと、検索は次のルールへ落ち、グループのトラフィックはエラーもなくもう一方のトンネルや通常のアップリンクを通って続く。それが許容できないなら、より小さい優先度番号で、その送信元に到達不能を返す終端ルールを追加し、損失が吸収されるのではなく見えるようにする。
送信元グループごとに新しい要求を送り、四つの事実を記録する——選ばれたテーブル、出力デバイス、変換された送信元、そして応答が同じ経路で戻ったかどうか。それからトンネルを一本落として繰り返す。インターフェース一覧に TUN デバイスが二つあることは、両方のデーモンが起動した証拠にすぎず、特定のクライアントがどちらを使ったかは何も語らない。トンネルが一本欠けた二回目の実行こそ、フォールバックのルールが意図どおりに働くかどうかを示す。