二つのDNS名、一つのOpenVPNポート:実際に何が振り分けるのか
同じアドレスを指す二つのホスト名は、やはり一つのリスナーに届く。バックエンドを選べるのはクライアントが送るフィールドだけで、素のOpenVPNはそれを送らない。
二つのAレコード、一つのアドレス、一つのデーモン——利用者がどの名前を打ち込んでも、届くパケットは同一であり、ポート1194に束縛されたプロセスには両者を見分ける材料がない。これは修理を待つ設定ミスではない。名前の解決が表現できることの限界である。二つのOpenVPNインスタンスのどちらかを選ぶには、クライアントが接続に入れ、フロントエンドが読み戻せる値でなければならず、標準のOpenVPNクライアントはそのような値を平文で入れない。以下では、それが何を意味するのか、そしてどの構成なら本当に通信を分けられるのかを見る。
レゾルバが返すものから始める
クライアントはvpn-a.exampleを問い合わせてアドレスを受け取る。そのアドレスとポートが、接続先のすべてである。名前は問い合わせの際にローカルで消費され、回線上にフィールドとして現れることはない。だから相手側のリスナーは、どのラベルが使われたのかを見ることはできない。vpn-a.exampleとvpn-b.exampleを同じホストに向ければ、どちらの問い合わせも同じ宛先を返す。だからデーモンは違いを記録することなく、二つの接続を一つのインスタンスに吸収する。アドレスベースのプロファイルが名前ベースのものとまったく同じ挙動をするのも、同じ理由による。
これは魅力的な回避策の一族も塞ぐ。追加の別名、一つのソケットで名前ごとに証明書を選ぶこと、問い合わせ名に紐づけたファイアウォール規則。いずれも、最初のパケットがクライアントを離れる前に捨てられた情報には作用できない。二つのサーバーを分けたまま保つ必要があるなら、分割は接続そのものと一緒に運ばれなければならない。つまりアドレス、ポート、あるいはプロトコルが定義するペイロードのフィールドである。
数えられる唯一の選択子はパケットの内側を通る
HTTPはHostヘッダーでこれを扱い、TLSはClientHello内のサーバー名拡張で扱う。だから一つのアドレスが、一つのリスナーの背後で多数のサイトに応える。どちらも、回線上のプロトコルが特定のサービスへの要求を運ぶから機能する。OpenVPNについての重要な事実は、中間装置が検査する層に、どちらのフィールドも存在しないことだ。
標準的な構成では、制御チャネルを守るTLSハンドシェイクは外側のセッションが確立した後に起こり、その内側で運ばれる。したがってそのセッションを終端していないリスナーには、不透明なストリームとして見える。先頭バイトのオペコードとフレーミングは識別できるので、OpenVPNだとは分かるが、サーバー名があるとすればそれは暗号化された部分に座っている。外側のパケットから仮想ホストを読もうとする設計は、プロトコルがそもそもそこに書かないフィールドを読んでいる。これはトンネルの作り方に由来するもので、より賢いパターン照合で埋まる隙間ではない。
プロトコル分別器が届かないところ
sslhプロジェクトが文書化している種類のマルチプレクサは、一つのポートで待ち受け、先頭バイトを検査し、認識したプロトコルを話すバックエンドへ接続を渡す。一つのポートでSSH、HTTPS、OpenVPNを同時に扱う必要があるとき、これは確かな価値になる。
sslhはプロトコルごとに別のプローブを持ち、そのTLSプローブは本物のClientHelloからサーバー名やALPNの値を取り出せる。素のOpenVPNを認識することはTLS拡張を読むこととは別の分類であり、両者は組み合わさらない。待ち受けソケットにClientHelloを一度も送らないクライアントは、プローブに読むものを残さないからだ。したがってOpenVPNに一致する規則は「これはOpenVPNクライアントだ」と答えて、そこで止まる。その規則の背後の二つのOpenVPNバックエンドは区別できず、フロントエンドには選択の根拠がなく、どのセッションも同じ場所に着地する。多重化は一つのポートを別々のサービスで共有するものであり、一つのサービスの中で仮想ホスティングをするものではない。
二つのサーバーを本当に分ける構成
最も安価で正直な分割は、二つ目の公開アドレスをインスタンスごとに一つ持つことであり、既存のプロファイルは別々のアドレスを指しているので必要なものをもう備えている。次は同じアドレスの別ポートで、各プロファイルの数値以外に何のコストもかからない。TCPとUDPは別々のポート表を持つので、1194/tcpと1194/udpは無関係なソケットであり、それぞれ一つずつが同じホストに共存できる。これはトランスポートの分離であって名前による経路選択ではなく、しばしばそれと混同される。どちらの選択肢も、パケットが実際に含むものに規則が一致するので、ファイアウォールのレビューを通る。
必要な境界が二つの実装の間ではなく利用者のグループの間にあるなら、別々のクライアント設定、独自の証明書処理、明示的なアクセス方針を持つ一つのインスタンスでそれを覆える。それが買うのはサーバー内部の方針であり、サーバー間の分離ではない。両者は別の要件である。侵害下でも保たねばならない分離には、依然としてアドレスかファイアウォールの縁が要る。
OpenVPNの前に置いたTLS終端は、有能なフロントエンドに本物のサーバー名を晒せるが、クライアント側もその追加層向けに設定されている場合に限る。既存のプロファイルは、同じアドレスを指すだけではその挙動を得ない。
作る前に選択子を証明する
バックエンドを識別するフィールドを名指しし、次にクライアントが本当にそれを送るか確かめる。答えがアドレスなら計画は成り立ち、待ち受け側に決して届かないハンドシェイク内のサーバー名なら、どれだけDNSをいじっても救えない。三つのコマンドで待ち受け側はすぐに決着する。
ss -lntup | grep -E ':443|:1194'
openssl s_client -connect vpn-a.example:443 -servername vpn-a.example </dev/null | head -n 20
journalctl -u openvpn-server@server-a -n 20 --no-pager
opensslの行は、そのアドレスで何かがTLSを終端している場所でしか証明書を返さない。素のOpenVPNリスナーに対しては停止するか失敗し、それ自体が外側の層が何を晒すかの答えになる。journalの行は、各バックエンドで読めば、どのインスタンスがセッションを受け入れたかを示す。フロントエンドのログは分類したことを記録し、意図したことは決して記録しない。だから利用者がどこに着地したかの権威は、バックエンドだけに残る。
別々のサーバーに届くように作った二つのプロファイルで締めくくり、一つずつ接続して、それぞれのバックエンドのログを記す。両方のセッションが同じjournalに現れるなら、選択子は設定ミスではなく存在しないのであり、直すべきはクライアントが狙えるアドレスかポートである。経路規則を編集しても助けにはならない。規則には経路選択の材料が一度も渡されていないからだ。分割が本物になったら、次の人が二つ目の別名を足して仕事をしてくれると期待しないよう、プロファイルのそばに書き残す。