client-connect スクリプトからアドレスを返すと、クライアントの IP を設定できるように見えますが、実際はできません。フックがサーバーとやり取りする唯一の経路は一時ファイルであり、そのパスはスクリプトの最後の引数として渡されます。アドレスはそこに ifconfig-push 構文で書き込まなければなりません。アドレスを出力したり、何も書かずにゼロで終了したりするスクリプトは、プールから割り当てられたアドレスを持つ接続済みクライアントを生み出すだけで、サーバーログはまったく正常に見えます。

混乱はもっともです。ほかの多くのフックは終了コードや環境変数を通じて実際に応答します。このフックには独自の契約があり、その契約はファイル名から始まります。

フックが書くのはファイルであり、戻り値ではない

宣言は 二行です。script-security 2 で呼び出しを許可し、次に client-connect /etc/openvpn/server/assign-address.sh を置きます。クライアントが認証されると、サーバーは生成されたファイルパスを最後の引数として追加してこのスクリプトを実行します。そのパスが $1 になるのは、宣言が独自の追加引数を渡していない場合だけです。そのクライアントに適用されるものはすべて、通常のディレクティブとして 一行に一つずつ、設定ファイルに現れるのとまったく同じ形でそのファイルに書き込まれます。独自のパラメーターを追加するラッパーは位置をずらしてしまい、典型的な症状は --verbose のようなパスへ書き込むスクリプトです。

終了ステータスは契約のもう半分です。ゼロは受け入れてファイルを読むことを意味し、それ以外は接続を拒否します。マッピングされていない ID が黙ってプールに戻るのではなく拒否されるのは、この仕組みによるものです。成功を返す前に完全なディレクティブを書き終えてください、そうすれば中途半端に書かれたファイルがクライアントの設定になることはありません。

二つのディレクティブは、ほかのすべてと相互作用するため注意が必要です。ifconfig-push はアドレスを固定し、プッシュされた route はトンネル経由で何に到達できるかをクライアントに伝えます。前者だけ返して後者を忘れると、アドレスはあるのに行き先がないクライアントになります。ファイル内の順序は重要ではありませんが、網羅性は重要です。どのピアも到達できないプレフィックスへのルートをプッシュするフックは、反対側から見た同じ誤りです。

入力は環境変数で渡されます。common_name、trusted_ip、ifconfig_pool_remote_ip などです。これらはすべてサーバー外部から来る文字列として扱い、シェルの断片として扱ってはいけません。

アドレスのマッピングはポリシーであり、算術ではない

証明書の名前を解析し、その数字を最後のオクテットとして使うのは魅力的な近道です。しかしそれはアロケーターではなく、ありふれた形で失敗します。同じ接尾辞を持つ 二つの名前は衝突し、300 で終わる名前はアドレスではなく、管理者が名前を変えた証明書はデバイスを黙って移動させます。呼び出しのたびにサブネットへの所属、使用可能なホスト範囲、予約アドレス、一意性を検証し、マッピングは人間が読める表に保ってください。

名前を制御する者がアドレスを制御するので、この配備で common_name が実際に何を表すかを決めてください。username-as-common-name が有効な場合、ID は証明書のサブジェクトではなくログイン名です。その値をシェル評価に渡してはいけませんし、クライアントにファイルパスを左右させてもいけません。

レジストリに支えられたアロケーターは並行性に耐える必要があります。アドレスをアトミックに予約し、書き込みに失敗した場合は中途半端に設定されたクライアントを受け入れずに処理し、各割り当てをそれを所有する接続でタグ付けしてください。そうすれば、古いセッションの遅い切断が、新しいセッションがすでに保持しているアドレスを解放することはできません。一つのプールを共有する複数のサーバーインスタンスは、この要件を任意ではなく、より厳しいものにします。

トポロジーが ifconfig-push の意味を決める

10.77.0.0/24 で topology subnet を使う場合、ifconfig-push 10.77.0.20 255.255.255.0 はアドレスとそのネットマスクです。topology net30 の場合、二番目の引数はネットマスクではなくピアのアドレスです。そのためサブネット形式を net30 のサーバーにコピーすると、接続はできても通信を通せないクライアントになります。トポロジーは意図的に変更してください。例を一貫して見せるために変えてはいけません。すでに net30 で動いているサーバーには古いアドレス体系のクライアントが接続中なので、切り替えはメンテナンス時間を伴う移行であり、一行の編集ではありません。

静的アドレスは動的プールの外側に置き、ほかのすべての静的割り当ての外側にも置く必要があります。通常の client-config-dir ファイルが配るものも含みます。二つの仕組みが異なるクライアントに同じアドレスを割り当てられるなら、遅かれ早かれそのうちの一つが実際にそうします。

信頼する前に 四つのケースを試す

マッピングされた ID、マッピングされていない ID、再接続、レジストリ障害のシミュレーションに対して同じチェックを実行してください。openvpn --config client.ovpn --verb 4 はプッシュ応答に割り当てアドレスを表示し、サーバー側では grep -A2 PUSH_REPLY /var/log/openvpn/openvpn.log が実際に何が送られたかを確認できます。マッピングされていない ID では、期待される結果はプールのアドレスではなく接続の拒否です。

次に、デーモンがそれらの一時ファイルを書き込むディレクトリを調べます。スクリプトは権限を落としたサービスのコンテキストで実行されるため、root は書けてもデーモンが書けないパスでは、すべての接続が拒否になり、権限エラーはログの数行下に埋もれます。

割り当ては認可ではありません。固定したアドレスは、サーバーの背後のすべての宛先へのアクセスを許すものではありません。ルーティングとファイアウォールのポリシーは独自の層に保ち、いま固定したアドレスに対して検証してください。

関連記事