OpenVPN のフックスクリプトは、たった今作ったトンネルを使えるのか
OpenVPN は自分自身の起動処理の中でフックを実行するため、トンネルを必要とするスクリプトは、そのスクリプトを待っているトンネルを待つことになる。作業を切り離し、フックはイベントで選ぶ。
フックスクリプトがトンネルの向こう側に届くかどうかは、どのイベントに結び付いているか、スクリプトの実行時に OpenVPN がまだ起動シーケンスの内側にいるか、そしてその作業がシーケンスから切り離されているかという三つの条件で決まる。ほとんどのスクリプトは三つ目で失敗する。待ち時間を足すのはライフサイクルの問題をタイミングの問題として扱うことであり、誤解を招く程度にしか効かない。
まずイベントを決め、次に作業を遅延させる仕組みを決め、最後に遅延した作業を走らせる場所を決める。この三つの判断は独立しており、それらを混同することが、二行のフックを丸一日の作業に変える。
フックが自分を待つトンネルを待つ理由
OpenVPNは、パケットを動かすのと同じプロセスの内側で、フックを同期的に呼び出す。up は tun または tap デバイスが存在した後、ただしルートが追加される前に発火し、route-up はルートの後に発火するが、どちらの瞬間も相手が応答することを約束しない。どちらのフックのスクリプトが動いている間もデーモンは転送しておらず、トンネルをトラフィックが通ることを期待するコマンドは、そのコマンドを待つトンネルを待つ。この循環待ちが、手動での実行は通るのにフックはタイムアウトする理由である。
明白な対処、つまり数秒眠ることは時々うまくいくが、それは遠端が勝手に上がってきたからであり、眠ることが何かを直したからではない。準備状態を問い合わせるヘルパーは別の答えで、フックは戻り、転送が始まり、ヘルパーは実際にそこに在るときに必要なものを得る。
便利さではなくイベントでフックを選ぶ
up はデバイスだけを必要とする作業に、route-up はルートを必要とする作業に結び付け、client-connect と client-disconnect はサーバー側のクライアントごとの処理のために取っておく。切断時の後片付けは、持続的なインターフェースでの再接続が完全な再起動と同じ順序をたどらないため、二度実行されても耐えなければならない。サーバーでは client-connect はデーモンの起動時ではなくクライアントの認証時に発火し、ロジックをそちらへ移す前に確認する価値がある。作業が単一の接続ではなくサービスのライフサイクルに属するなら、systemd ユニットのほうがどのフックよりも良い住みかだ。
フックのコマンドはデーモンが直接実行し、間にシェルを挟まないため、up の行にある ifconfig_remote は文字どおりのテキストとして届く。コマンドを明示的なシェルで包むか、ロジックを環境を読むスクリプトに置く。ipchange は独自の引数を足す指示子で、アドレスとポートをコマンドに渡すため、どれも想定していないスクリプトは予期しない二つを見ることになる。
作業を切り離す三つの方法
第一の選択肢は、フックの内側からヘルパーをバックグラウンドに送り、setsid か systemd-run --scope で起動し、継承したディスクリプタを閉じてすぐにゼロで終了することだ。第二は、フックに小さな状態ファイルを書かせ、systemd のタイマーかパスユニットにそれへ働かせることで、フックは些細なままで、再試行の方針はログのある場所に置ける。第三は、フックを完全に捨てて、VPN サービスの後に並ぶユニットへ作業を移すことで、作業がそもそも接続ごとでないならこれが正しい答えである。三つとも一つの規則を共有する:フックはトンネルを必要とする前に戻らなければならない。
どれを選んでも、ヘルパーは冪等で観察可能にする。重なり合う二つの呼び出しが同じ取り返しのつかない操作を二度実行してはならず、失敗するヘルパーは人間が読めるものを残さなければならない。
サンドボックスの失敗とライフサイクルの失敗を分ける
スクリプトが root のプロンプトから動き、サービスとしては失敗するとき、原因は通常ネットワークではなくサンドボックスだ。制限の厳しい ProtectHome、読み取り専用の ProtectSystem、あるいは存在しない作業ディレクトリは、スクリプトやそのデータを届かない場所に置きうる。SELinux のホストでは、ファイルに実行を許すコンテキストも必要だ。ユニットは systemctl cat openvpn-server@server で、ジャーナルは journalctl -u openvpn-server@server -n 80 で読み、接続のタイムアウトではなく権限やコンテキストのエラーを探し、サンドボックスを無効にするのは最初の手ではなく最後の手であるべきだ。
ライフサイクルの失敗は見え方が違い、フックは始まり、ヘルパーは動き、ソケットはまだそこにないだけだ。フック自身からタイムスタンプ付きの行を書き、切断して再接続し、イベントを順に読む。ヘルパーが本当にトンネルを必要とするなら、固定時間眠るのではなく準備状態を問い合わせさせる。
三つの項目、つまりどのフックが動いたか、どのアカウントを使ったか、どこで止まったかが、次の報告を役に立つものにする。それらがなければ、後の調査は毎回最初からやり直しになる。