client-connect スクリプトが非ゼロで終了すると、OpenVPN はクライアントを拒否します。そして運用者は、対応する client-disconnect スクリプトが後始末をしてくれると考えがちです。実際にはそれはほとんど実行されません。disconnect の処理は、connect 段階が何を完了できたかに条件づけられているからです。拒否とロールバックを一つの仕組みと見なすことが、失敗したログインを漏れたファイアウォール規則や孤立した記録に変えてしまいます。両者は別々の義務であり、自動なのは一方だけです。状態が残ったとサーバーを責める前に、フック自身の終了経路を読んでください。

非ゼロの終了はクライアントを拒否するが、作業を元に戻さない

client-connect の終了ステータスは受け入れの判断です。ゼロは受け入れ、それ以外はインスタンスを完全に確立する前に破棄することを意味します。その契約のどこにも、スクリプトが外部世界にすでに行ったことを検査する部分はありません。スクリプトが iptables の規則を追加し、行を書き込み、カウンタを増やしたなら、それらの変更は拒否後も残ります。

同じ非対称性はプラグインにも当てはまります。一つのインスタンスに対して複数の connect 関数が実行されることがあるためです。後からの失敗は前の成功を遡って取り消しませんし、前の関数には兄弟の関数が失敗したことを知る方法がありません。

拒否は自動、後片付けは自動ではない

OpenVPN 2.6 のマニュアルは、client-disconnect の処理を、先行する connect 処理の成功に結びつけています。文書化された例外はカスケード連鎖を対象とします。ある connect ステップがすでに成功を返していれば、後続の connect ステップが失敗しても、インスタンス破棄時に対応する disconnect ステップは実行されます。その例外は狭く、設定されたコールバックの数に依存するため、シェルスクリプトを 一つ試しても連鎖については何も分かりません。

設計する前に実際の構成を把握してください。どのスクリプトが、どのプラグインが、どの順序で動くのか。下の表は、中途半端な設定が何を残すかを示しています。

What the hook did before failingWhat the client seesWhat stays behind
Created a firewall entryA rejection, with no tunnelA rule matching an address nobody owns
Wrote a state rowA rejection, with no tunnelA record of a session that never started
Allocated a tunnel addressA rejection, with no tunnelAn address marked busy until something releases it

どの行もクライアントにとっては同じ結末で、ホストにとっては異なる結末です。クライアントはどれが起きたかを知ることができないため、フックは失敗ステータスを返す前に自分が行ったことを巻き戻さなければなりません。

失敗したスクリプトの中で補償する

共有状態に触れる前に、検証できるものはすべて検証してください。証明書の項目、アドレスの空き状況、クライアントが属するグループです。最も安価な拒否は、割り当てが何も起きる前に起きる拒否です。設定が複数の外部操作にまたがるようになったら、どれが完了したかを記録し、失敗ステータスを返す前に逆順に巻き戻してください。

具体例:スクリプトがクライアントごとのファイアウォール項目を作成し、その後にそれを説明する状態行の書き込みに失敗します。失敗した分岐は、作成したばかりの項目を削除してから非ゼロで終了すべきです。スケジュールされないかもしれない disconnect イベントを待つことが、規則が本来のセッションより長く生き残る理由です。

補償を冪等にし、所有権を持たせる

後片付けは 二回実行しても安全で、何を削除してよいかについて正確でなければなりません。以前の試行からの遅れたイベントが、共通の名前や再利用されたトンネルアドレスを共有しているという理由だけで、より新しいセッションの割り当てを削除してはなりません。具体的なハンドルや操作 ID をリソースとともに記録し、名前ではなくそのキーで削除してください。データベースのトランザクションはデータベース自身の変更にしか役立ちません。その境界の外にあるファイアウォール規則には、明示的な補償手順が必要です。

変更と後片付けの間でプロセスが死ぬ可能性を前提とし、外部状態を定期的に受け入れ済みクライアント一覧と比較する照合処理を追加してください。

実際に異なる 三つの経路をテストする

三つのケースを演習してください。何も割り当てられる前の拒否、部分的な割り当ての後の拒否、そして後で終了する正常な受け入れ済みセッションです。この 三つは異なるログ行と異なる残留物を生みます。それぞれの後に外部状態を検証し、クライアントの失敗メッセージだけを見ないでください。三つのうち 二つでは、クライアントの失敗こそが期待される結果だからです。

フックの段階、終了ステータス、何が割り当てられ解放されたかを、認証情報を除いて記録してください。作成したリソースを名指しするフックのログこそが、漏れた規則を半年後に見つけられるようにします。

関連記事