同じ名前で二つ目の接続がすでに始まった後に切断レコードが届くと、そのユーザーの最新行を更新するハンドラは誤ったセッションを閉じます。誤りはタイムスタンプの比較そのものではありません。名前と一秒全体の組み合わせが一つのセッションを識別するという前提が誤りです。フックイベントは接続を記述し、接続にはそれだけに属するキーが必要です。

切断変数が実際に何を意味するかから始めてください。そのとき初めて識別子の選択が意味を持ちます。

切断フックが実際に運ぶもの

Community OpenVPN 2.6 では、フックリファレンス が time_unix をクライアントが接続した瞬間として定義しており、これは接続フックの実行前に設定され、切断フックが後で発火しても再定義されません。time_duration はそのセッションの長さを秒単位で返すので、終了時刻はおおよそ接続時刻に期間を足したものです。どちらも整数秒であり、どちらもデータベースがイベントを処理した瞬間ではありません。これら三つの時刻を分けて保つことが、テスト中だけ正しく見えるのではなく、相関を実際に成立させます。

この計算は、フック自体が提供する唯一の終了時刻だからこそ、書き留める価値があります。処理遅延やホスト間の時計のずれは、依然としてその外側にあります。

明白な組が一意でない理由

名前と接続時刻を組み合わせると検索範囲は狭まりますが、それでもセッションを識別できません。クライアントは同じ秒のうちに再接続できるため、二つのセッションが両方の値を共有できます。二つのデーモンインスタンスや、同じ設定を保つサーバー入れ替えが、異なるプロセスからその組を生み出します。トンネルアドレスも同じ理由で再利用されるため、それはセッションの属性であり、セッションの識別子ではありません。

障害の仕方は静かです。最初のセッションに対する遅れた切断が最新行と一致し、二つ目のセッションがまだ使っている割り当てを解除してしまうため、利用者は自分が何もしていないのに突然アクセスを失います。後から見て正しく見える行数が、問題の発見を難しくします。

安定した識別子はどこから来るか

管理インターフェースは各クライアントにクライアントインスタンス識別子を割り当て、その識別子は発行したデーモンインスタンスに限定されます。実用的な外部キーは、安定したサーバー識別子、デーモンの起動時に変わるエポック、そのクライアント ID を組み合わせます。エポックが重要なのは、識別子がプロセスとともに再起動し、裸のプロセス ID が後から無関係なものに再利用されうるからです。

子の接続スクリプト内で設定された環境変数は、それらのスクリプトが独自の環境を持つ別プロセスとして実行されるため、親デーモンの後の切断スクリプトには渡りません。したがって接続時に UUID を生成するのは半分の解決策です。切断側が同じ UUID を名指しできなければならず、つまり識別子は両方のイベントが届く場所に存在する必要があります。

解除を条件付きかつ冪等にする

解除はユーザーではなくセッションの所有者で行います。名前に現在属する割り当てを削除するハンドラは、いずれ誤ったものを削除しますが、唯一の防御は、削除するレコードがこのイベントの記述するレコードであることを確認することです。

管理インターフェースは同時に一つの接続された管理クライアントしか受け付けないので、同じソケットを奪い合う二つ目のオブザーバーを取り付けるのではなく、すでに所有している側と統合してください。

繰り返されるイベントは結果を二度変えずに処理し、対になっていないイベントは捨てずに照合のために保持してください。切断コールバックには文書化されたライフサイクル条件があり、クラッシュや強制停止の後には決して届かないことがあるため、設計には終了イベントが欠けたセッションを表す方法が必要です。

一秒以内の二回の再接続、二つのデーモンインスタンス、処理に失敗した接続、セッション途中のデーモン再起動といった面倒なケースを意図的に試してください。この四つすべてに耐えるキーだけが信頼に値します。

関連記事